qt 6.5.1 original

This commit is contained in:
kleuter
2023-10-29 23:33:08 +01:00
parent 71d22ab6b0
commit 85d238dfda
21202 changed files with 5499099 additions and 0 deletions

View File

@ -0,0 +1,20 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
add_subdirectory(qstringlistmodel)
if(TARGET Qt::Gui)
add_subdirectory(qabstractitemmodel)
add_subdirectory(qabstractproxymodel)
add_subdirectory(qconcatenatetablesproxymodel)
add_subdirectory(qidentityproxymodel)
add_subdirectory(qitemselectionmodel)
add_subdirectory(qsortfilterproxymodel_recursive)
add_subdirectory(qsortfilterproxymodel_regularexpression)
add_subdirectory(qtransposeproxymodel)
endif()
if(TARGET Qt::Widgets)
add_subdirectory(qsortfilterproxymodel)
endif()
if(TARGET Qt::Sql AND TARGET Qt::Widgets)
add_subdirectory(qitemmodel)
endif()

View File

@ -0,0 +1,16 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qabstractitemmodel Test:
#####################################################################
qt_internal_add_test(tst_qabstractitemmodel
SOURCES
../../../other/qabstractitemmodelutils/dynamictreemodel.cpp ../../../other/qabstractitemmodelutils/dynamictreemodel.h
tst_qabstractitemmodel.cpp
INCLUDE_DIRECTORIES
../../../other/qabstractitemmodelutils
LIBRARIES
Qt::Gui
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qabstractproxymodel Test:
#####################################################################
qt_internal_add_test(tst_qabstractproxymodel
SOURCES
tst_qabstractproxymodel.cpp
LIBRARIES
Qt::Gui
Qt::TestPrivate
)

View File

@ -0,0 +1,639 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QtTest/private/qpropertytesthelper_p.h>
#include <qabstractproxymodel.h>
#include <QItemSelection>
#include <qstandarditemmodel.h>
class tst_QAbstractProxyModel : public QObject
{
Q_OBJECT
private slots:
void qabstractproxymodel();
void data_data();
void data();
void flags_data();
void flags();
void headerData_data();
void headerData();
void headerDataInBounds();
void itemData_data();
void itemData();
void mapFromSource_data();
void mapFromSource();
void mapSelectionFromSource_data();
void mapSelectionFromSource();
void mapSelectionToSource_data();
void mapSelectionToSource();
void mapToSource_data();
void mapToSource();
void revert();
void setSourceModel();
void submit_data();
void submit();
void testRoleNames();
void testSwappingRowsProxy();
void testDragAndDrop();
void sourceModelBinding();
};
// Subclass that exposes the protected functions.
class SubQAbstractProxyModel : public QAbstractProxyModel
{
public:
// QAbstractProxyModel::mapFromSource is a pure virtual function.
QModelIndex mapFromSource(QModelIndex const& sourceIndex) const override
{ Q_UNUSED(sourceIndex); return QModelIndex(); }
// QAbstractProxyModel::mapToSource is a pure virtual function.
QModelIndex mapToSource(QModelIndex const& proxyIndex) const override
{ Q_UNUSED(proxyIndex); return QModelIndex(); }
QModelIndex index(int, int, const QModelIndex&) const override
{
return QModelIndex();
}
QModelIndex parent(const QModelIndex&) const override
{
return QModelIndex();
}
int rowCount(const QModelIndex&) const override
{
return 0;
}
int columnCount(const QModelIndex&) const override
{
return 0;
}
};
void tst_QAbstractProxyModel::qabstractproxymodel()
{
SubQAbstractProxyModel model;
model.data(QModelIndex());
model.flags(QModelIndex());
model.headerData(0, Qt::Vertical, 0);
model.itemData(QModelIndex());
model.mapFromSource(QModelIndex());
model.mapSelectionFromSource(QItemSelection());
model.mapSelectionToSource(QItemSelection());
model.mapToSource(QModelIndex());
model.revert();
model.setSourceModel(0);
QCOMPARE(model.sourceModel(), (QAbstractItemModel*)0);
model.submit();
}
void tst_QAbstractProxyModel::data_data()
{
QTest::addColumn<QModelIndex>("proxyIndex");
QTest::addColumn<int>("role");
QTest::addColumn<QVariant>("data");
QTest::newRow("null") << QModelIndex() << 0 << QVariant();
}
// public QVariant data(QModelIndex const& proxyIndex, int role = Qt::DisplayRole) const
void tst_QAbstractProxyModel::data()
{
QFETCH(QModelIndex, proxyIndex);
QFETCH(int, role);
QFETCH(QVariant, data);
SubQAbstractProxyModel model;
QCOMPARE(model.data(proxyIndex, role), data);
}
Q_DECLARE_METATYPE(Qt::ItemFlags)
void tst_QAbstractProxyModel::flags_data()
{
QTest::addColumn<QModelIndex>("index");
QTest::addColumn<Qt::ItemFlags>("flags");
QTest::newRow("null") << QModelIndex() << Qt::ItemFlags{};
}
// public Qt::ItemFlags flags(QModelIndex const& index) const
void tst_QAbstractProxyModel::flags()
{
QFETCH(QModelIndex, index);
QFETCH(Qt::ItemFlags, flags);
SubQAbstractProxyModel model;
QCOMPARE(model.flags(index), flags);
}
Q_DECLARE_METATYPE(Qt::Orientation)
Q_DECLARE_METATYPE(Qt::ItemDataRole)
void tst_QAbstractProxyModel::headerData_data()
{
QTest::addColumn<int>("section");
QTest::addColumn<Qt::Orientation>("orientation");
QTest::addColumn<Qt::ItemDataRole>("role");
QTest::addColumn<QVariant>("headerData");
QTest::newRow("null") << 0 << Qt::Vertical << Qt::UserRole << QVariant();
}
// public QVariant headerData(int section, Qt::Orientation orientation, int role) const
void tst_QAbstractProxyModel::headerData()
{
QFETCH(int, section);
QFETCH(Qt::Orientation, orientation);
QFETCH(Qt::ItemDataRole, role);
QFETCH(QVariant, headerData);
SubQAbstractProxyModel model;
QCOMPARE(model.headerData(section, orientation, role), headerData);
}
class SimpleTableReverseColumnsProxy : public QAbstractProxyModel
{
public:
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return {};
if (row < 0 || row >= rowCount() || column < 0 || column >= columnCount())
qFatal("error"); // cannot QFAIL here
return createIndex(row, column);
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return 0;
return sourceModel()->rowCount();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid())
return 0;
return sourceModel()->columnCount();
}
QModelIndex parent(const QModelIndex &) const override
{
return QModelIndex();
}
QModelIndex mapToSource(const QModelIndex &idx) const override
{
if (!idx.isValid())
return QModelIndex();
return sourceModel()->index(idx.row(), columnCount() - 1 - idx.column());
}
QModelIndex mapFromSource(const QModelIndex &idx) const override
{
if (idx.parent().isValid())
return QModelIndex();
return createIndex(idx.row(), columnCount() - 1 - idx.column());
}
};
void tst_QAbstractProxyModel::headerDataInBounds()
{
QStandardItemModel qsim(0, 5);
qsim.setHorizontalHeaderLabels({"Col1", "Col2", "Col3", "Col4", "Col5"});
SimpleTableReverseColumnsProxy proxy;
QSignalSpy headerDataChangedSpy(&proxy, &QAbstractItemModel::headerDataChanged);
QVERIFY(headerDataChangedSpy.isValid());
proxy.setSourceModel(&qsim);
QCOMPARE(proxy.rowCount(), 0);
QCOMPARE(proxy.columnCount(), 5);
for (int i = 0; i < proxy.columnCount(); ++i) {
QString expected = QString("Col%1").arg(i + 1);
QCOMPARE(proxy.headerData(i, Qt::Horizontal).toString(), expected);
}
qsim.appendRow({
new QStandardItem("A"),
new QStandardItem("B"),
new QStandardItem("C"),
new QStandardItem("D"),
new QStandardItem("E")
});
QCOMPARE(proxy.rowCount(), 1);
QCOMPARE(proxy.columnCount(), 5);
QCOMPARE(headerDataChangedSpy.size(), 1);
QCOMPARE(headerDataChangedSpy[0][0].value<Qt::Orientation>(), Qt::Horizontal);
QCOMPARE(headerDataChangedSpy[0][1].value<int>(), 0);
QCOMPARE(headerDataChangedSpy[0][2].value<int>(), 4);
for (int i = 0; i < proxy.columnCount(); ++i) {
QString expected = QString("Col%1").arg(proxy.columnCount() - i);
QCOMPARE(proxy.headerData(i, Qt::Horizontal).toString(), expected);
}
qsim.appendRow({
new QStandardItem("A"),
new QStandardItem("B"),
new QStandardItem("C"),
new QStandardItem("D"),
new QStandardItem("E")
});
QCOMPARE(proxy.rowCount(), 2);
QCOMPARE(proxy.columnCount(), 5);
QCOMPARE(headerDataChangedSpy.size(), 1);
for (int i = 0; i < proxy.columnCount(); ++i) {
QString expected = QString("Col%1").arg(proxy.columnCount() - i);
QCOMPARE(proxy.headerData(i, Qt::Horizontal).toString(), expected);
}
QVERIFY(qsim.removeRows(0, 1));
QCOMPARE(proxy.rowCount(), 1);
QCOMPARE(proxy.columnCount(), 5);
QCOMPARE(headerDataChangedSpy.size(), 1);
for (int i = 0; i < proxy.columnCount(); ++i) {
QString expected = QString("Col%1").arg(proxy.columnCount() - i);
QCOMPARE(proxy.headerData(i, Qt::Horizontal).toString(), expected);
}
QVERIFY(qsim.removeRows(0, 1));
QCOMPARE(proxy.rowCount(), 0);
QCOMPARE(proxy.columnCount(), 5);
QCOMPARE(headerDataChangedSpy.size(), 2);
QCOMPARE(headerDataChangedSpy[1][0].value<Qt::Orientation>(), Qt::Horizontal);
QCOMPARE(headerDataChangedSpy[1][1].value<int>(), 0);
QCOMPARE(headerDataChangedSpy[1][2].value<int>(), 4);
for (int i = 0; i < proxy.columnCount(); ++i) {
QString expected = QString("Col%1").arg(i + 1);
QCOMPARE(proxy.headerData(i, Qt::Horizontal).toString(), expected);
}
}
void tst_QAbstractProxyModel::itemData_data()
{
QTest::addColumn<QModelIndex>("index");
QTest::addColumn<int>("count");
QTest::newRow("null") << QModelIndex() << 0;
}
// public QMap<int,QVariant> itemData(QModelIndex const& index) const
void tst_QAbstractProxyModel::itemData()
{
QFETCH(QModelIndex, index);
QFETCH(int, count);
SubQAbstractProxyModel model;
QCOMPARE(model.itemData(index).size(), count);
}
void tst_QAbstractProxyModel::mapFromSource_data()
{
QTest::addColumn<QModelIndex>("sourceIndex");
QTest::addColumn<QModelIndex>("mapFromSource");
QTest::newRow("null") << QModelIndex() << QModelIndex();
}
// public QModelIndex mapFromSource(QModelIndex const& sourceIndex) const
void tst_QAbstractProxyModel::mapFromSource()
{
QFETCH(QModelIndex, sourceIndex);
QFETCH(QModelIndex, mapFromSource);
SubQAbstractProxyModel model;
QCOMPARE(model.mapFromSource(sourceIndex), mapFromSource);
}
void tst_QAbstractProxyModel::mapSelectionFromSource_data()
{
QTest::addColumn<QItemSelection>("selection");
QTest::addColumn<QItemSelection>("mapSelectionFromSource");
QTest::newRow("null") << QItemSelection() << QItemSelection();
QTest::newRow("empty") << QItemSelection(QModelIndex(), QModelIndex()) << QItemSelection(QModelIndex(), QModelIndex());
}
// public QItemSelection mapSelectionFromSource(QItemSelection const& selection) const
void tst_QAbstractProxyModel::mapSelectionFromSource()
{
QFETCH(QItemSelection, selection);
QFETCH(QItemSelection, mapSelectionFromSource);
SubQAbstractProxyModel model;
QCOMPARE(model.mapSelectionFromSource(selection), mapSelectionFromSource);
}
void tst_QAbstractProxyModel::mapSelectionToSource_data()
{
QTest::addColumn<QItemSelection>("selection");
QTest::addColumn<QItemSelection>("mapSelectionToSource");
QTest::newRow("null") << QItemSelection() << QItemSelection();
QTest::newRow("empty") << QItemSelection(QModelIndex(), QModelIndex()) << QItemSelection(QModelIndex(), QModelIndex());
}
// public QItemSelection mapSelectionToSource(QItemSelection const& selection) const
void tst_QAbstractProxyModel::mapSelectionToSource()
{
QFETCH(QItemSelection, selection);
QFETCH(QItemSelection, mapSelectionToSource);
SubQAbstractProxyModel model;
QCOMPARE(model.mapSelectionToSource(selection), mapSelectionToSource);
}
void tst_QAbstractProxyModel::mapToSource_data()
{
QTest::addColumn<QModelIndex>("proxyIndex");
QTest::addColumn<QModelIndex>("mapToSource");
QTest::newRow("null") << QModelIndex() << QModelIndex();
}
// public QModelIndex mapToSource(QModelIndex const& proxyIndex) const
void tst_QAbstractProxyModel::mapToSource()
{
QFETCH(QModelIndex, proxyIndex);
QFETCH(QModelIndex, mapToSource);
SubQAbstractProxyModel model;
QCOMPARE(model.mapToSource(proxyIndex), mapToSource);
}
// public void revert()
void tst_QAbstractProxyModel::revert()
{
SubQAbstractProxyModel model;
model.revert();
}
// public void setSourceModel(QAbstractItemModel* sourceModel)
void tst_QAbstractProxyModel::setSourceModel()
{
SubQAbstractProxyModel model;
QCOMPARE(model.property("sourceModel"), QVariant::fromValue<QAbstractItemModel*>(0));
QStandardItemModel *sourceModel = new QStandardItemModel(&model);
model.setSourceModel(sourceModel);
QCOMPARE(model.sourceModel(), static_cast<QAbstractItemModel*>(sourceModel));
QCOMPARE(model.property("sourceModel").value<QObject*>(), static_cast<QObject*>(sourceModel));
QCOMPARE(model.property("sourceModel").value<QAbstractItemModel*>(), sourceModel);
QStandardItemModel *sourceModel2 = new QStandardItemModel(&model);
model.setSourceModel(sourceModel2);
QCOMPARE(model.sourceModel(), static_cast<QAbstractItemModel*>(sourceModel2));
QCOMPARE(model.property("sourceModel").value<QObject*>(), static_cast<QObject*>(sourceModel2));
QCOMPARE(model.property("sourceModel").value<QAbstractItemModel*>(), sourceModel2);
delete sourceModel2;
QCOMPARE(model.sourceModel(), static_cast<QAbstractItemModel*>(0));
}
void tst_QAbstractProxyModel::submit_data()
{
QTest::addColumn<bool>("submit");
QTest::newRow("null") << true;
}
// public bool submit()
void tst_QAbstractProxyModel::submit()
{
QFETCH(bool, submit);
SubQAbstractProxyModel model;
QCOMPARE(model.submit(), submit);
}
class StandardItemModelWithCustomRoleNames : public QStandardItemModel
{
public:
enum CustomRole {
CustomRole1 = Qt::UserRole,
CustomRole2
};
QHash<int, QByteArray> roleNames() const override
{
auto result = QStandardItemModel::roleNames();
result.insert(CustomRole1, QByteArrayLiteral("custom1"));
result.insert(CustomRole2, QByteArrayLiteral("custom2"));
return result;
}
};
class AnotherStandardItemModelWithCustomRoleNames : public QStandardItemModel
{
public:
enum CustomRole {
AnotherCustomRole1 = Qt::UserRole + 10, // Different to StandardItemModelWithCustomRoleNames::CustomRole1
AnotherCustomRole2
};
QHash<int, QByteArray> roleNames() const override
{
return {{AnotherCustomRole1, QByteArrayLiteral("another_custom1")},
{AnotherCustomRole2, QByteArrayLiteral("another_custom2")}};
}
};
/**
Verifies that @p subSet is a subset of @p superSet. That is, all keys in @p subSet exist in @p superSet and have the same values.
*/
static void verifySubSetOf(const QHash<int, QByteArray> &superSet, const QHash<int, QByteArray> &subSet)
{
QHash<int, QByteArray>::const_iterator it = subSet.constBegin();
const QHash<int, QByteArray>::const_iterator end = subSet.constEnd();
for ( ; it != end; ++it ) {
QVERIFY(superSet.contains(it.key()));
QCOMPARE(it.value(), superSet.value(it.key()));
}
}
void tst_QAbstractProxyModel::testRoleNames()
{
QStandardItemModel defaultModel;
StandardItemModelWithCustomRoleNames model;
QHash<int, QByteArray> rootModelRoleNames = model.roleNames();
QHash<int, QByteArray> defaultModelRoleNames = defaultModel.roleNames();
verifySubSetOf( rootModelRoleNames, defaultModelRoleNames);
QVERIFY( rootModelRoleNames.size() == defaultModelRoleNames.size() + 2 );
QVERIFY( rootModelRoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole1));
QVERIFY( rootModelRoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole2));
QVERIFY( rootModelRoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole1) == "custom1" );
QVERIFY( rootModelRoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole2) == "custom2" );
SubQAbstractProxyModel proxy1;
proxy1.setSourceModel(&model);
QHash<int, QByteArray> proxy1RoleNames = proxy1.roleNames();
verifySubSetOf( proxy1RoleNames, defaultModelRoleNames );
QVERIFY( proxy1RoleNames.size() == defaultModelRoleNames.size() + 2 );
QVERIFY( proxy1RoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole1));
QVERIFY( proxy1RoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole2));
QVERIFY( proxy1RoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole1) == "custom1" );
QVERIFY( proxy1RoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole2) == "custom2" );
SubQAbstractProxyModel proxy2;
proxy2.setSourceModel(&proxy1);
QHash<int, QByteArray> proxy2RoleNames = proxy2.roleNames();
verifySubSetOf( proxy2RoleNames, defaultModelRoleNames );
QVERIFY( proxy2RoleNames.size() == defaultModelRoleNames.size() + 2 );
QVERIFY( proxy2RoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole1));
QVERIFY( proxy2RoleNames.contains(StandardItemModelWithCustomRoleNames::CustomRole2));
QVERIFY( proxy2RoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole1) == "custom1" );
QVERIFY( proxy2RoleNames.value(StandardItemModelWithCustomRoleNames::CustomRole2) == "custom2" );
}
// This class only supports very simple table models
class SwappingProxy : public QAbstractProxyModel
{
static int swapRow(const int row)
{
if (row == 2) {
return 3;
} else if (row == 3) {
return 2;
} else {
return row;
}
}
public:
virtual QModelIndex index(int row, int column, const QModelIndex &parentIdx) const override
{
if (!sourceModel())
return QModelIndex();
if (row < 0 || column < 0)
return QModelIndex();
if (row >= sourceModel()->rowCount())
return QModelIndex();
if (column >= sourceModel()->columnCount())
return QModelIndex();
return createIndex(row, column, parentIdx.internalPointer());
}
virtual QModelIndex parent(const QModelIndex &parentIdx) const override
{
// well, we're a 2D model
Q_UNUSED(parentIdx);
return QModelIndex();
}
virtual int rowCount(const QModelIndex &parentIdx) const override
{
if (parentIdx.isValid() || !sourceModel())
return 0;
return sourceModel()->rowCount();
}
virtual int columnCount(const QModelIndex &parentIdx) const override
{
if (parentIdx.isValid() || !sourceModel())
return 0;
return sourceModel()->rowCount();
}
virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const override
{
if (!proxyIndex.isValid())
return QModelIndex();
if (!sourceModel())
return QModelIndex();
Q_ASSERT(!proxyIndex.parent().isValid());
return sourceModel()->index(swapRow(proxyIndex.row()), proxyIndex.column(), QModelIndex());
}
virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override
{
if (!sourceIndex.isValid())
return QModelIndex();
if (!sourceModel())
return QModelIndex();
Q_ASSERT(!sourceIndex.parent().isValid());
return index(swapRow(sourceIndex.row()), sourceIndex.column(), QModelIndex());
}
};
void tst_QAbstractProxyModel::testSwappingRowsProxy()
{
QStandardItemModel defaultModel;
defaultModel.setRowCount(4);
defaultModel.setColumnCount(2);
for (int row = 0; row < defaultModel.rowCount(); ++row) {
defaultModel.setItem(row, 0, new QStandardItem(QString::number(row) + QLatin1Char('A')));
defaultModel.setItem(row, 1, new QStandardItem(QString::number(row) + QLatin1Char('B')));
}
SwappingProxy proxy;
proxy.setSourceModel(&defaultModel);
QCOMPARE(proxy.data(proxy.index(0, 0, QModelIndex())), QVariant("0A"));
QCOMPARE(proxy.data(proxy.index(0, 1, QModelIndex())), QVariant("0B"));
QCOMPARE(proxy.data(proxy.index(1, 0, QModelIndex())), QVariant("1A"));
QCOMPARE(proxy.data(proxy.index(1, 1, QModelIndex())), QVariant("1B"));
QCOMPARE(proxy.data(proxy.index(2, 0, QModelIndex())), QVariant("3A"));
QCOMPARE(proxy.data(proxy.index(2, 1, QModelIndex())), QVariant("3B"));
QCOMPARE(proxy.data(proxy.index(3, 0, QModelIndex())), QVariant("2A"));
QCOMPARE(proxy.data(proxy.index(3, 1, QModelIndex())), QVariant("2B"));
for (int row = 0; row < defaultModel.rowCount(); ++row) {
QModelIndex left = proxy.index(row, 0, QModelIndex());
QModelIndex right = proxy.index(row, 1, QModelIndex());
QCOMPARE(left.siblingAtColumn(1), right);
QCOMPARE(right.siblingAtColumn(0), left);
}
}
class StandardItemModelWithCustomDragAndDrop : public QStandardItemModel
{
public:
QStringList mimeTypes() const override { return QStringList() << QStringLiteral("foo/mimetype"); }
Qt::DropActions supportedDragActions() const override { return Qt::CopyAction | Qt::LinkAction; }
Qt::DropActions supportedDropActions() const override { return Qt::MoveAction; }
};
void tst_QAbstractProxyModel::testDragAndDrop()
{
StandardItemModelWithCustomDragAndDrop sourceModel;
SubQAbstractProxyModel proxy;
proxy.setSourceModel(&sourceModel);
QCOMPARE(proxy.mimeTypes(), sourceModel.mimeTypes());
QCOMPARE(proxy.supportedDragActions(), sourceModel.supportedDragActions());
QCOMPARE(proxy.supportedDropActions(), sourceModel.supportedDropActions());
}
void tst_QAbstractProxyModel::sourceModelBinding()
{
SubQAbstractProxyModel proxy;
QStandardItemModel model1;
QStandardItemModel model2;
QTestPrivate::testReadWritePropertyBasics<QAbstractProxyModel, QAbstractItemModel *>(
proxy, &model1, &model2, "sourceModel");
if (QTest::currentTestFailed()) {
qDebug("Failed model - model test");
return;
}
proxy.setSourceModel(&model2);
QTestPrivate::testReadWritePropertyBasics<QAbstractProxyModel, QAbstractItemModel *>(
proxy, &model1, nullptr, "sourceModel");
if (QTest::currentTestFailed()) {
qDebug("Failed model - nullptr test");
return;
}
proxy.setSourceModel(&model1);
QTestPrivate::testReadWritePropertyBasics<QAbstractProxyModel, QAbstractItemModel *>(
proxy, nullptr, &model2, "sourceModel");
if (QTest::currentTestFailed()) {
qDebug("Failed nullptr - model test");
return;
}
}
QTEST_MAIN(tst_QAbstractProxyModel)
#include "tst_qabstractproxymodel.moc"

View File

@ -0,0 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qconcatenatetablesproxymodel Test:
#####################################################################
qt_internal_add_test(tst_qconcatenatetablesproxymodel
SOURCES
tst_qconcatenatetablesproxymodel.cpp
LIBRARIES
Qt::Gui
)

View File

@ -0,0 +1,867 @@
// Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QSignalSpy>
#include <QSortFilterProxyModel>
#include <QTest>
#include <QStandardItemModel>
#include <QIdentityProxyModel>
#include <QItemSelectionModel>
#include <QMimeData>
#include <QStringListModel>
#include <QAbstractItemModelTester>
#include <qconcatenatetablesproxymodel.h>
Q_DECLARE_METATYPE(QModelIndex)
// Extracts a full row from a model as a string
// Works best if every cell contains only one character
static QString extractRowTexts(QAbstractItemModel *model, int row, const QModelIndex &parent = QModelIndex())
{
QString result;
const int colCount = model->columnCount();
for (int col = 0; col < colCount; ++col) {
const QString txt = model->index(row, col, parent).data().toString();
result += txt.isEmpty() ? QStringLiteral(" ") : txt;
}
return result;
}
// Extracts a full column from a model as a string
// Works best if every cell contains only one character
static QString extractColumnTexts(QAbstractItemModel *model, int column, const QModelIndex &parent = QModelIndex())
{
QString result;
const int rowCount = model->rowCount();
for (int row = 0; row < rowCount; ++row) {
const QString txt = model->index(row, column, parent).data().toString();
result += txt.isEmpty() ? QStringLiteral(" ") : txt;
}
return result;
}
static QString rowSpyToText(const QSignalSpy &spy)
{
if (!spy.isValid())
return QStringLiteral("THE SIGNALSPY IS INVALID!");
QString str;
for (int i = 0; i < spy.size(); ++i) {
str += spy.at(i).at(1).toString() + QLatin1Char(',') + spy.at(i).at(2).toString();
if (i + 1 < spy.size())
str += QLatin1Char(';');
}
return str;
}
class tst_QConcatenateTablesProxyModel : public QObject
{
Q_OBJECT
private Q_SLOTS:
void init();
void shouldAggregateTwoModelsCorrectly();
void shouldAggregateThenRemoveTwoEmptyModelsCorrectly();
void shouldAggregateTwoEmptyModelsWhichThenGetFilled();
void shouldHandleDataChanged();
void shouldHandleSetData();
void shouldHandleSetItemData();
void shouldHandleRowInsertionAndRemoval();
void shouldAggregateAnotherModelThenRemoveModels();
void shouldUseSmallestColumnCount();
void shouldIncreaseColumnCountWhenRemovingFirstModel();
void shouldHandleColumnInsertionAndRemoval();
void shouldPropagateLayoutChanged();
void shouldReactToModelReset();
void shouldUpdateColumnsOnModelReset();
void shouldPropagateDropOnItem_data();
void shouldPropagateDropOnItem();
void shouldPropagateDropBetweenItems();
void shouldPropagateDropBetweenItemsAtModelBoundary();
void shouldPropagateDropAfterLastRow_data();
void shouldPropagateDropAfterLastRow();
void qtbug91788();
void qtbug91878();
void createPersistentOnLayoutAboutToBeChanged();
private:
QStandardItemModel mod;
QStandardItemModel mod2;
QStandardItemModel mod3;
};
void tst_QConcatenateTablesProxyModel::init()
{
// Prepare some source models to use later on
mod.clear();
mod.appendRow({ new QStandardItem(QStringLiteral("A")), new QStandardItem(QStringLiteral("B")), new QStandardItem(QStringLiteral("C")) });
mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
mod.setVerticalHeaderLabels(QStringList() << QStringLiteral("One"));
mod2.clear();
mod2.appendRow({ new QStandardItem(QStringLiteral("D")), new QStandardItem(QStringLiteral("E")), new QStandardItem(QStringLiteral("F")) });
mod2.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
mod2.setVerticalHeaderLabels(QStringList() << QStringLiteral("Two"));
mod3.clear();
mod3.appendRow({ new QStandardItem(QStringLiteral("1")), new QStandardItem(QStringLiteral("2")), new QStandardItem(QStringLiteral("3")) });
mod3.appendRow({ new QStandardItem(QStringLiteral("4")), new QStandardItem(QStringLiteral("5")), new QStandardItem(QStringLiteral("6")) });
}
void tst_QConcatenateTablesProxyModel::shouldAggregateTwoModelsCorrectly()
{
// Given a combining proxy
QConcatenateTablesProxyModel pm;
// When adding two source models
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
// Then the proxy should show 2 rows
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
// ... and correct headers
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
QVERIFY(!pm.canFetchMore(QModelIndex()));
}
void tst_QConcatenateTablesProxyModel::shouldAggregateThenRemoveTwoEmptyModelsCorrectly()
{
// Given a combining proxy
QConcatenateTablesProxyModel pm;
// When adding two empty models
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
QIdentityProxyModel i1, i2;
pm.addSourceModel(&i1);
pm.addSourceModel(&i2);
// Then the proxy should still be empty (and no signals emitted)
QCOMPARE(pm.rowCount(), 0);
QCOMPARE(pm.columnCount(), 0);
QCOMPARE(rowATBISpy.size(), 0);
QCOMPARE(rowInsertedSpy.size(), 0);
// When removing the empty models
pm.removeSourceModel(&i1);
pm.removeSourceModel(&i2);
// Then the proxy should still be empty (and no signals emitted)
QCOMPARE(pm.rowCount(), 0);
QCOMPARE(pm.columnCount(), 0);
QCOMPARE(rowATBRSpy.size(), 0);
QCOMPARE(rowRemovedSpy.size(), 0);
}
void tst_QConcatenateTablesProxyModel::shouldAggregateTwoEmptyModelsWhichThenGetFilled()
{
// Given a combining proxy with two empty models
QConcatenateTablesProxyModel pm;
QIdentityProxyModel i1, i2;
pm.addSourceModel(&i1);
pm.addSourceModel(&i2);
// When filling them afterwards
i1.setSourceModel(&mod);
i2.setSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
// Then the proxy should show 2 rows
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(pm.columnCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
// ... and correct headers
QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
QVERIFY(!pm.canFetchMore(QModelIndex()));
}
void tst_QConcatenateTablesProxyModel::shouldHandleDataChanged()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
// When a cell in a source model changes
mod.item(0, 0)->setData("a", Qt::EditRole);
// Then the change should be notified to the proxy
QCOMPARE(dataChangedSpy.size(), 1);
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
// Same test with the other model
mod2.item(0, 2)->setData("f", Qt::EditRole);
QCOMPARE(dataChangedSpy.size(), 2);
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
}
void tst_QConcatenateTablesProxyModel::shouldHandleSetData()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
// When changing a cell using setData
pm.setData(pm.index(0, 0), "a");
// Then the change should be notified to the proxy
QCOMPARE(dataChangedSpy.size(), 1);
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
// Same test with the other model
pm.setData(pm.index(1, 2), "f");
QCOMPARE(dataChangedSpy.size(), 2);
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
}
void tst_QConcatenateTablesProxyModel::shouldHandleSetItemData()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
// When changing a cell using setData
pm.setItemData(pm.index(0, 0), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("X")),
std::make_pair<int, QVariant>(Qt::UserRole, 88) });
// Then the change should be notified to the proxy
QCOMPARE(dataChangedSpy.size(), 1);
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("XBC"));
QCOMPARE(pm.index(0, 0).data(Qt::UserRole).toInt(), 88);
// Same test with the other model
pm.setItemData(pm.index(1, 2), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("Y")),
std::make_pair<int, QVariant>(Qt::UserRole, 89) });
QCOMPARE(dataChangedSpy.size(), 2);
QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEY"));
QCOMPARE(pm.index(1, 2).data(Qt::UserRole).toInt(), 89);
}
void tst_QConcatenateTablesProxyModel::shouldHandleRowInsertionAndRemoval()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
// When a source model inserts a new row
QList<QStandardItem *> row;
row.append(new QStandardItem(QStringLiteral("1")));
row.append(new QStandardItem(QStringLiteral("2")));
row.append(new QStandardItem(QStringLiteral("3")));
mod2.insertRow(0, row);
// Then the proxy should notify its users and show changes
QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("1,1"));
QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("1,1"));
QCOMPARE(pm.rowCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
// When removing that row
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
mod2.removeRow(0);
// Then the proxy should notify its users and show changes
QCOMPARE(rowATBRSpy.size(), 1);
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
QCOMPARE(rowRemovedSpy.size(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
// When removing the last row from mod2
rowATBRSpy.clear();
rowRemovedSpy.clear();
mod2.removeRow(0);
// Then the proxy should notify its users and show changes
QCOMPARE(rowATBRSpy.size(), 1);
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
QCOMPARE(rowRemovedSpy.size(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
QCOMPARE(pm.rowCount(), 1);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
}
void tst_QConcatenateTablesProxyModel::shouldAggregateAnotherModelThenRemoveModels()
{
// Given two models combined, and a third model
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
// When adding the new source model
pm.addSourceModel(&mod3);
// Then the proxy should notify its users about the two rows inserted
QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("2,3"));
QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("2,3"));
QCOMPARE(pm.rowCount(), 4);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
// When removing that source model again
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
pm.removeSourceModel(&mod3);
// Then the proxy should notify its users about the row removed
QCOMPARE(rowATBRSpy.size(), 1);
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 2);
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 3);
QCOMPARE(rowRemovedSpy.size(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 2);
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 3);
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
// When removing model 2
rowATBRSpy.clear();
rowRemovedSpy.clear();
pm.removeSourceModel(&mod2);
QCOMPARE(rowATBRSpy.size(), 1);
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
QCOMPARE(rowRemovedSpy.size(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
QCOMPARE(pm.rowCount(), 1);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
// When removing model 1
rowATBRSpy.clear();
rowRemovedSpy.clear();
pm.removeSourceModel(&mod);
QCOMPARE(rowATBRSpy.size(), 1);
QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 0);
QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 0);
QCOMPARE(rowRemovedSpy.size(), 1);
QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 0);
QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 0);
QCOMPARE(pm.rowCount(), 0);
}
void tst_QConcatenateTablesProxyModel::shouldUseSmallestColumnCount()
{
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
pm.addSourceModel(&mod2);
mod2.setColumnCount(1);
pm.addSourceModel(&mod3);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(pm.rowCount(), 4);
QCOMPARE(pm.columnCount(), 1);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("1"));
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("4"));
const QModelIndex indexA = pm.mapFromSource(mod.index(0, 0));
QVERIFY(indexA.isValid());
QCOMPARE(indexA, pm.index(0, 0));
const QModelIndex indexB = pm.mapFromSource(mod.index(0, 1));
QVERIFY(!indexB.isValid());
const QModelIndex indexD = pm.mapFromSource(mod2.index(0, 0));
QVERIFY(indexD.isValid());
QCOMPARE(indexD, pm.index(1, 0));
// Test setData in an ignored column (QTBUG-91253)
QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
mod.setData(mod.index(0, 1), "b");
QCOMPARE(dataChangedSpy.size(), 0);
// Test dataChanged across all columns, some visible, some ignored
mod.dataChanged(mod.index(0, 0), mod.index(0, 2));
QCOMPARE(dataChangedSpy.size(), 1);
QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
QCOMPARE(dataChangedSpy.at(0).at(1).toModelIndex(), pm.index(0, 0));
}
void tst_QConcatenateTablesProxyModel::shouldIncreaseColumnCountWhenRemovingFirstModel()
{
// Given a model with 2 columns and one with 3 columns
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
QAbstractItemModelTester modelTest(&pm, this);
mod.setColumnCount(2);
pm.addSourceModel(&mod2);
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(pm.columnCount(), 2);
QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
// When removing the first source model
pm.removeSourceModel(&mod);
// Then the proxy should notify its users about the row removed, and the column added
QCOMPARE(pm.rowCount(), 1);
QCOMPARE(pm.columnCount(), 3);
QCOMPARE(rowSpyToText(rowATBRSpy), QStringLiteral("0,0"));
QCOMPARE(rowSpyToText(rowRemovedSpy), QStringLiteral("0,0"));
QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("DEF"));
}
void tst_QConcatenateTablesProxyModel::shouldHandleColumnInsertionAndRemoval()
{
// Given two models combined, one with 2 columns and one with 3
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
QAbstractItemModelTester modelTest(&pm, this);
mod.setColumnCount(2);
pm.addSourceModel(&mod2);
QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
// When the first source model inserts a new column
QCOMPARE(mod.columnCount(), 2);
mod.setColumnCount(3);
// Then the proxy should notify its users and show changes
QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(pm.columnCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("AB "));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
// And when removing two columns
mod.setColumnCount(1);
// Then the proxy should notify its users and show changes
QCOMPARE(rowSpyToText(colATBRSpy), QStringLiteral("1,2"));
QCOMPARE(rowSpyToText(colRemovedSpy), QStringLiteral("1,2"));
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(pm.columnCount(), 1);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
}
void tst_QConcatenateTablesProxyModel::shouldPropagateLayoutChanged()
{
// Given two source models, the second one being a QSFPM
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
QAbstractItemModelTester modelTest(&pm, this);
QSortFilterProxyModel qsfpm;
qsfpm.setSourceModel(&mod3);
pm.addSourceModel(&qsfpm);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
// And a selection (row 1)
QItemSelectionModel selection(&pm);
selection.select(pm.index(1, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
const QModelIndexList lst = selection.selectedIndexes();
QCOMPARE(lst.size(), 3);
for (int col = 0; col < lst.size(); ++col) {
QCOMPARE(lst.at(col).row(), 1);
QCOMPARE(lst.at(col).column(), col);
}
QSignalSpy layoutATBCSpy(&pm, SIGNAL(layoutAboutToBeChanged()));
QSignalSpy layoutChangedSpy(&pm, SIGNAL(layoutChanged()));
// When changing the sorting in the QSFPM
qsfpm.sort(0, Qt::DescendingOrder);
// Then the proxy should emit the layoutChanged signals, and show re-sorted data
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
QCOMPARE(layoutATBCSpy.size(), 1);
QCOMPARE(layoutChangedSpy.size(), 1);
// And the selection should be updated accordingly (it became row 2)
const QModelIndexList lstAfter = selection.selectedIndexes();
QCOMPARE(lstAfter.size(), 3);
for (int col = 0; col < lstAfter.size(); ++col) {
QCOMPARE(lstAfter.at(col).row(), 2);
QCOMPARE(lstAfter.at(col).column(), col);
}
}
void tst_QConcatenateTablesProxyModel::shouldReactToModelReset()
{
// Given two source models, the second one being a QSFPM
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod);
QAbstractItemModelTester modelTest(&pm, this);
QSortFilterProxyModel qsfpm;
qsfpm.setSourceModel(&mod3);
pm.addSourceModel(&qsfpm);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
// When changing the source model of the QSFPM
qsfpm.setSourceModel(&mod2);
// Then the proxy should emit the reset signals, and show the new data
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
QCOMPARE(rowATBRSpy.size(), 0);
QCOMPARE(rowRemovedSpy.size(), 0);
QCOMPARE(rowATBISpy.size(), 0);
QCOMPARE(rowInsertedSpy.size(), 0);
QCOMPARE(colATBRSpy.size(), 0);
QCOMPARE(colRemovedSpy.size(), 0);
QCOMPARE(modelATBResetSpy.size(), 1);
QCOMPARE(modelResetSpy.size(), 1);
}
void tst_QConcatenateTablesProxyModel::shouldUpdateColumnsOnModelReset()
{
// Given two source models, the first one being a QSFPM
QConcatenateTablesProxyModel pm;
QSortFilterProxyModel qsfpm;
qsfpm.setSourceModel(&mod3);
pm.addSourceModel(&qsfpm);
pm.addSourceModel(&mod);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("ABC"));
// ... and a model with only 2 columns
QStandardItemModel mod2Columns;
mod2Columns.appendRow({ new QStandardItem(QStringLiteral("W")), new QStandardItem(QStringLiteral("X")) });
QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
// When changing the source model of the QSFPM
qsfpm.setSourceModel(&mod2Columns);
// Then the proxy should reset, and show the new data
QCOMPARE(modelATBResetSpy.size(), 1);
QCOMPARE(modelResetSpy.size(), 1);
QCOMPARE(rowATBRSpy.size(), 0);
QCOMPARE(rowRemovedSpy.size(), 0);
QCOMPARE(rowATBISpy.size(), 0);
QCOMPARE(rowInsertedSpy.size(), 0);
QCOMPARE(colATBRSpy.size(), 0);
QCOMPARE(colRemovedSpy.size(), 0);
QCOMPARE(pm.rowCount(), 2);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("WX"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("AB"));
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem_data()
{
QTest::addColumn<int>("sourceRow");
QTest::addColumn<int>("destRow");
QTest::addColumn<QString>("expectedResult");
QTest::newRow("0-3") << 0 << 3 << QStringLiteral("ABCA");
QTest::newRow("1-2") << 1 << 2 << QStringLiteral("ABBD");
QTest::newRow("2-1") << 2 << 1 << QStringLiteral("ACCD");
QTest::newRow("3-0") << 3 << 0 << QStringLiteral("DBCD");
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem()
{
// Given two source models who handle drops
// Note: QStandardItemModel handles drop onto items by inserting child rows,
// which is good for QTreeView but not for QTableView or QConcatenateTablesProxyModel.
// So we use QStringListModel here instead.
QConcatenateTablesProxyModel pm;
QStringListModel model1({QStringLiteral("A"), QStringLiteral("B")});
QStringListModel model2({QStringLiteral("C"), QStringLiteral("D")});
pm.addSourceModel(&model1);
pm.addSourceModel(&model2);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(extractColumnTexts(&pm, 0), QStringLiteral("ABCD"));
// When dragging one item
QFETCH(int, sourceRow);
QMimeData* mimeData = pm.mimeData({pm.index(sourceRow, 0)});
QVERIFY(mimeData);
// and dropping onto another item
QFETCH(int, destRow);
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
delete mimeData;
// Then the result should be as expected
QFETCH(QString, expectedResult);
QCOMPARE(extractColumnTexts(&pm, 0), expectedResult);
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItems()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod3);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(pm.rowCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
// When dragging the last row
QModelIndexList indexes;
indexes.reserve(pm.columnCount());
for (int col = 0; col < pm.columnCount(); ++col) {
indexes.append(pm.index(2, col));
}
QMimeData* mimeData = pm.mimeData(indexes);
QVERIFY(mimeData);
// and dropping it before row 1
const int destRow = 1;
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
delete mimeData;
// Then a new row should be inserted
QCOMPARE(pm.rowCount(), 4);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItemsAtModelBoundary()
{
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod3);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(pm.rowCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
// When dragging the first row
QModelIndexList indexes;
indexes.reserve(pm.columnCount());
for (int col = 0; col < pm.columnCount(); ++col) {
indexes.append(pm.index(0, col));
}
QMimeData* mimeData = pm.mimeData(indexes);
QVERIFY(mimeData);
// and dropping it before row 2
const int destRow = 2;
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
delete mimeData;
// Then a new row should be inserted
QCOMPARE(pm.rowCount(), 4);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
// and it should be part of the second model
QCOMPARE(mod2.rowCount(), 2);
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow_data()
{
QTest::addColumn<int>("destRow");
// Dropping after the last row is documented to be done with destRow == -1.
QTest::newRow("-1") << -1;
// However, sometimes QTreeView calls dropMimeData with destRow == rowCount...
// Not sure if that's a bug or not, but let's support it in the model, just in case.
QTest::newRow("3") << 3;
}
void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow()
{
QFETCH(int, destRow);
// Given two models combined
QConcatenateTablesProxyModel pm;
pm.addSourceModel(&mod3);
pm.addSourceModel(&mod2);
QAbstractItemModelTester modelTest(&pm, this);
QCOMPARE(pm.rowCount(), 3);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
// When dragging the second row
QModelIndexList indexes;
indexes.reserve(pm.columnCount());
for (int col = 0; col < pm.columnCount(); ++col) {
indexes.append(pm.index(1, col));
}
QMimeData* mimeData = pm.mimeData(indexes);
QVERIFY(mimeData);
// and dropping it after the last row
QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
delete mimeData;
// Then a new row should be inserted at the end
QCOMPARE(pm.rowCount(), 4);
QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
}
void tst_QConcatenateTablesProxyModel::qtbug91788()
{
QConcatenateTablesProxyModel proxyConcat;
QStringList strList{QString("one"),QString("two")};
QStringListModel strListModelA(strList);
QSortFilterProxyModel proxyFilter;
proxyFilter.setSourceModel(&proxyConcat);
proxyConcat.addSourceModel(&strListModelA);
proxyConcat.removeSourceModel(&strListModelA); // This should not assert
QCOMPARE(proxyConcat.columnCount(), 0);
}
void tst_QConcatenateTablesProxyModel::qtbug91878()
{
QStandardItemModel m;
m.setRowCount(4);
m.setColumnCount(4);
QConcatenateTablesProxyModel pm;
QSortFilterProxyModel proxyFilter;
proxyFilter.setSourceModel(&pm);
proxyFilter.setFilterFixedString("something");
pm.addSourceModel(&m); // This should not assert
QCOMPARE(pm.columnCount(), 4);
QCOMPARE(pm.rowCount(), 4);
}
void tst_QConcatenateTablesProxyModel::createPersistentOnLayoutAboutToBeChanged() // QTBUG-93466
{
QStandardItemModel model1(3, 1);
QStandardItemModel model2(3, 1);
for (int row = 0; row < 3; ++row) {
model1.setData(model1.index(row, 0), row);
model2.setData(model2.index(row, 0), row + 5);
}
QConcatenateTablesProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.addSourceModel(&model1);
proxy.addSourceModel(&model2);
QList<QPersistentModelIndex> idxList;
QSignalSpy layoutAboutToBeChangedSpy(&proxy, &QAbstractItemModel::layoutAboutToBeChanged);
QSignalSpy layoutChangedSpy(&proxy, &QAbstractItemModel::layoutChanged);
connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, this, [&idxList, &proxy](){
idxList.clear();
for (int row = 0; row < 3; ++row)
idxList << QPersistentModelIndex(proxy.index(row, 0));
});
connect(&proxy, &QAbstractItemModel::layoutChanged, this, [&idxList](){
QCOMPARE(idxList.size(), 3);
QCOMPARE(idxList.at(0).row(), 1);
QCOMPARE(idxList.at(0).column(), 0);
QCOMPARE(idxList.at(0).data().toInt(), 0);
QCOMPARE(idxList.at(1).row(), 0);
QCOMPARE(idxList.at(1).column(), 0);
QCOMPARE(idxList.at(1).data().toInt(), -1);
QCOMPARE(idxList.at(2).row(), 2);
QCOMPARE(idxList.at(2).column(), 0);
QCOMPARE(idxList.at(2).data().toInt(), 2);
});
QVERIFY(model1.setData(model1.index(1, 0), -1));
model1.sort(0);
QCOMPARE(layoutAboutToBeChangedSpy.size(), 1);
QCOMPARE(layoutChangedSpy.size(), 1);
}
QTEST_GUILESS_MAIN(tst_QConcatenateTablesProxyModel)
#include "tst_qconcatenatetablesproxymodel.moc"

View File

@ -0,0 +1,16 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qidentityproxymodel Test:
#####################################################################
qt_internal_add_test(tst_qidentityproxymodel
SOURCES
../../../other/qabstractitemmodelutils/dynamictreemodel.cpp ../../../other/qabstractitemmodelutils/dynamictreemodel.h
tst_qidentityproxymodel.cpp
INCLUDE_DIRECTORIES
../../../other/qabstractitemmodelutils
LIBRARIES
Qt::Gui
)

View File

@ -0,0 +1,517 @@
// Copyright (C) 2011 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Stephen Kelly <stephen.kelly@kdab.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QAbstractItemModelTester>
#include <QCoreApplication>
#include <QSignalSpy>
#include <QStandardItemModel>
#include <QStringListModel>
#include <QTest>
#include <QTransposeProxyModel>
#include <QLoggingCategory>
#include "dynamictreemodel.h"
#include "qidentityproxymodel.h"
Q_LOGGING_CATEGORY(lcItemModels, "qt.corelib.tests.itemmodels")
class DataChangedModel : public QAbstractListModel
{
public:
int rowCount(const QModelIndex &parent) const override { return parent.isValid() ? 0 : 1; }
QVariant data(const QModelIndex&, int) const override { return QVariant(); }
QModelIndex index(int row, int column, const QModelIndex &) const override { return createIndex(row, column); }
void changeData()
{
const QModelIndex idx = index(0, 0, QModelIndex());
Q_EMIT dataChanged(idx, idx, QList<int>() << 1);
}
};
class tst_QIdentityProxyModel : public QObject
{
Q_OBJECT
public:
tst_QIdentityProxyModel();
public slots:
void initTestCase();
void cleanupTestCase();
void cleanup();
private slots:
void insertRows();
void removeRows();
void moveRows();
void moveColumns();
void reset();
void dataChanged();
void itemData();
void persistIndexOnLayoutChange();
void createPersistentOnLayoutAboutToBeChanged();
protected:
void verifyIdentity(QAbstractItemModel *model, const QModelIndex &parent = QModelIndex());
private:
QStandardItemModel *m_model;
QIdentityProxyModel *m_proxy;
QAbstractItemModelTester *m_modelTest;
};
tst_QIdentityProxyModel::tst_QIdentityProxyModel()
: m_model(0), m_proxy(0)
{
}
void tst_QIdentityProxyModel::initTestCase()
{
qRegisterMetaType<QList<int> >();
m_model = new QStandardItemModel(0, 1);
m_proxy = new QIdentityProxyModel();
m_modelTest = new QAbstractItemModelTester(m_proxy, this);
}
void tst_QIdentityProxyModel::cleanupTestCase()
{
delete m_proxy;
delete m_model;
delete m_modelTest;
}
void tst_QIdentityProxyModel::cleanup()
{
m_model->clear();
m_model->insertColumns(0, 1);
}
void tst_QIdentityProxyModel::verifyIdentity(QAbstractItemModel *model, const QModelIndex &parent)
{
const int rows = model->rowCount(parent);
const int columns = model->columnCount(parent);
const QModelIndex proxyParent = m_proxy->mapFromSource(parent);
QCOMPARE(m_proxy->mapToSource(proxyParent), parent);
QCOMPARE(rows, m_proxy->rowCount(proxyParent));
QCOMPARE(columns, m_proxy->columnCount(proxyParent));
for (int row = 0; row < rows; ++row) {
for (int column = 0; column < columns; ++column) {
const QModelIndex idx = model->index(row, column, parent);
const QModelIndex proxyIdx = m_proxy->mapFromSource(idx);
QCOMPARE(proxyIdx.model(), m_proxy);
QCOMPARE(m_proxy->mapToSource(proxyIdx), idx);
QVERIFY(proxyIdx.isValid());
QCOMPARE(proxyIdx.row(), row);
QCOMPARE(proxyIdx.column(), column);
QCOMPARE(proxyIdx.parent(), proxyParent);
QCOMPARE(proxyIdx.data(), idx.data());
QCOMPARE(proxyIdx.flags(), idx.flags());
const int childCount = m_proxy->rowCount(proxyIdx);
const bool hasChildren = m_proxy->hasChildren(proxyIdx);
QCOMPARE(model->hasChildren(idx), hasChildren);
QVERIFY((childCount > 0) == hasChildren);
if (hasChildren)
verifyIdentity(model, idx);
}
}
}
/*
tests
*/
void tst_QIdentityProxyModel::insertRows()
{
QStandardItem *parentItem = m_model->invisibleRootItem();
for (int i = 0; i < 4; ++i) {
QStandardItem *item = new QStandardItem(QLatin1String("item ") + QString::number(i));
parentItem->appendRow(item);
parentItem = item;
}
m_proxy->setSourceModel(m_model);
verifyIdentity(m_model);
QSignalSpy modelBeforeSpy(m_model, &QStandardItemModel::rowsAboutToBeInserted);
QSignalSpy modelAfterSpy(m_model, &QStandardItemModel::rowsInserted);
QSignalSpy proxyBeforeSpy(m_proxy, &QStandardItemModel::rowsAboutToBeInserted);
QSignalSpy proxyAfterSpy(m_proxy, &QStandardItemModel::rowsInserted);
QVERIFY(modelBeforeSpy.isValid());
QVERIFY(modelAfterSpy.isValid());
QVERIFY(proxyBeforeSpy.isValid());
QVERIFY(proxyAfterSpy.isValid());
QStandardItem *item = new QStandardItem(QString("new item"));
parentItem->appendRow(item);
QVERIFY(modelBeforeSpy.size() == 1 && 1 == proxyBeforeSpy.size());
QVERIFY(modelAfterSpy.size() == 1 && 1 == proxyAfterSpy.size());
QCOMPARE(modelBeforeSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(1), proxyBeforeSpy.first().at(1));
QCOMPARE(modelBeforeSpy.first().at(2), proxyBeforeSpy.first().at(2));
QCOMPARE(modelAfterSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(1), proxyAfterSpy.first().at(1));
QCOMPARE(modelAfterSpy.first().at(2), proxyAfterSpy.first().at(2));
verifyIdentity(m_model);
}
void tst_QIdentityProxyModel::removeRows()
{
QStandardItem *parentItem = m_model->invisibleRootItem();
for (int i = 0; i < 4; ++i) {
QStandardItem *item = new QStandardItem(QLatin1String("item ") + QString::number(i));
parentItem->appendRow(item);
parentItem = item;
}
m_proxy->setSourceModel(m_model);
verifyIdentity(m_model);
QSignalSpy modelBeforeSpy(m_model, &QStandardItemModel::rowsAboutToBeRemoved);
QSignalSpy modelAfterSpy(m_model, &QStandardItemModel::rowsRemoved);
QSignalSpy proxyBeforeSpy(m_proxy, &QStandardItemModel::rowsAboutToBeRemoved);
QSignalSpy proxyAfterSpy(m_proxy, &QStandardItemModel::rowsRemoved);
QVERIFY(modelBeforeSpy.isValid());
QVERIFY(modelAfterSpy.isValid());
QVERIFY(proxyBeforeSpy.isValid());
QVERIFY(proxyAfterSpy.isValid());
const QModelIndex topLevel = m_model->index(0, 0, QModelIndex());
const QModelIndex secondLevel = m_model->index(0, 0, topLevel);
const QModelIndex thirdLevel = m_model->index(0, 0, secondLevel);
QVERIFY(thirdLevel.isValid());
m_model->removeRow(0, secondLevel);
QVERIFY(modelBeforeSpy.size() == 1 && 1 == proxyBeforeSpy.size());
QVERIFY(modelAfterSpy.size() == 1 && 1 == proxyAfterSpy.size());
QCOMPARE(modelBeforeSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(1), proxyBeforeSpy.first().at(1));
QCOMPARE(modelBeforeSpy.first().at(2), proxyBeforeSpy.first().at(2));
QCOMPARE(modelAfterSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(1), proxyAfterSpy.first().at(1));
QCOMPARE(modelAfterSpy.first().at(2), proxyAfterSpy.first().at(2));
verifyIdentity(m_model);
}
void tst_QIdentityProxyModel::moveRows()
{
QStringListModel model({"A", "B", "C", "D", "E", "F"});
m_proxy->setSourceModel(&model);
verifyIdentity(&model);
QSignalSpy modelBeforeSpy(&model, &QAbstractItemModel::rowsAboutToBeMoved);
QSignalSpy modelAfterSpy(&model, &QAbstractItemModel::rowsMoved);
QSignalSpy proxyBeforeSpy(m_proxy, &QAbstractItemModel::rowsAboutToBeMoved);
QSignalSpy proxyAfterSpy(m_proxy, &QAbstractItemModel::rowsMoved);
QVERIFY(m_proxy->moveRows(QModelIndex(), 1, 2, QModelIndex(), 5));
QCOMPARE(model.stringList(), QStringList({"A", "D", "E", "B", "C", "F"}));
QCOMPARE(modelBeforeSpy.size(), 1);
QCOMPARE(proxyBeforeSpy.size(), 1);
QCOMPARE(modelAfterSpy.size(), 1);
QCOMPARE(proxyAfterSpy.size(), 1);
QCOMPARE(modelBeforeSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(1), proxyBeforeSpy.first().at(1));
QCOMPARE(modelBeforeSpy.first().at(2), proxyBeforeSpy.first().at(2));
QCOMPARE(modelBeforeSpy.first().at(3).value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().at(3).value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(4), proxyBeforeSpy.first().at(4));
QCOMPARE(modelAfterSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(1), proxyAfterSpy.first().at(1));
QCOMPARE(modelAfterSpy.first().at(2), proxyAfterSpy.first().at(2));
QCOMPARE(modelAfterSpy.first().at(3).value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().at(3).value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(4), proxyAfterSpy.first().at(4));
QVERIFY(m_proxy->moveRows(QModelIndex(), 3, 2, QModelIndex(), 1));
QCOMPARE(model.stringList(), QStringList({"A", "B", "C", "D", "E", "F"}));
QVERIFY(m_proxy->moveRows(QModelIndex(), 0, 3, QModelIndex(), 6));
QCOMPARE(model.stringList(), QStringList({"D", "E", "F", "A", "B", "C"}));
verifyIdentity(&model);
m_proxy->setSourceModel(nullptr);
}
void tst_QIdentityProxyModel::moveColumns()
{
// QStringListModel implements moveRows but not moveColumns
// so we have to use a QTransposeProxyModel inbetween to check if
// QIdentityProxyModel::moveColumns works as expected
QStringListModel model({"A", "B", "C", "D", "E", "F"});
QTransposeProxyModel tpm;
tpm.setSourceModel(&model);
m_proxy->setSourceModel(&tpm);
verifyIdentity(&tpm);
QSignalSpy modelBeforeSpy(&tpm, &QAbstractItemModel::columnsAboutToBeMoved);
QSignalSpy modelAfterSpy(&tpm, &QAbstractItemModel::columnsMoved);
QSignalSpy proxyBeforeSpy(m_proxy, &QAbstractItemModel::columnsAboutToBeMoved);
QSignalSpy proxyAfterSpy(m_proxy, &QAbstractItemModel::columnsMoved);
QVERIFY(m_proxy->moveColumns(QModelIndex(), 1, 2, QModelIndex(), 5));
QCOMPARE(model.stringList(), QStringList({"A", "D", "E", "B", "C", "F"}));
QCOMPARE(proxyBeforeSpy.size(), 1);
QCOMPARE(modelBeforeSpy.size(), 1);
QCOMPARE(modelAfterSpy.size(), 1);
QCOMPARE(proxyAfterSpy.size(), 1);
QCOMPARE(modelBeforeSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(1), proxyBeforeSpy.first().at(1));
QCOMPARE(modelBeforeSpy.first().at(2), proxyBeforeSpy.first().at(2));
QCOMPARE(modelBeforeSpy.first().at(3).value<QModelIndex>(), m_proxy->mapToSource(proxyBeforeSpy.first().at(3).value<QModelIndex>()));
QCOMPARE(modelBeforeSpy.first().at(4), proxyBeforeSpy.first().at(4));
QCOMPARE(modelAfterSpy.first().first().value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().first().value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(1), proxyAfterSpy.first().at(1));
QCOMPARE(modelAfterSpy.first().at(2), proxyAfterSpy.first().at(2));
QCOMPARE(modelAfterSpy.first().at(3).value<QModelIndex>(), m_proxy->mapToSource(proxyAfterSpy.first().at(3).value<QModelIndex>()));
QCOMPARE(modelAfterSpy.first().at(4), proxyAfterSpy.first().at(4));
QVERIFY(m_proxy->moveColumns(QModelIndex(), 3, 2, QModelIndex(), 1));
QCOMPARE(model.stringList(), QStringList({"A", "B", "C", "D", "E", "F"}));
QVERIFY(m_proxy->moveColumns(QModelIndex(), 0, 3, QModelIndex(), 6));
QCOMPARE(model.stringList(), QStringList({"D", "E", "F", "A", "B", "C"}));
verifyIdentity(&tpm);
m_proxy->setSourceModel(nullptr);
}
void tst_QIdentityProxyModel::reset()
{
DynamicTreeModel model;
{
ModelInsertCommand insertCommand(&model);
insertCommand.setStartRow(0);
insertCommand.setEndRow(9);
insertCommand.doCommand();
}
{
ModelInsertCommand insertCommand(&model);
insertCommand.setAncestorRowNumbers(QList<int>() << 5);
insertCommand.setStartRow(0);
insertCommand.setEndRow(9);
insertCommand.doCommand();
}
m_proxy->setSourceModel(&model);
verifyIdentity(&model);
QSignalSpy modelBeforeSpy(&model, &DynamicTreeModel::modelAboutToBeReset);
QSignalSpy modelAfterSpy(&model, &DynamicTreeModel::modelReset);
QSignalSpy proxyBeforeSpy(m_proxy, &QIdentityProxyModel::modelAboutToBeReset);
QSignalSpy proxyAfterSpy(m_proxy, &QIdentityProxyModel::modelReset);
QVERIFY(modelBeforeSpy.isValid());
QVERIFY(modelAfterSpy.isValid());
QVERIFY(proxyBeforeSpy.isValid());
QVERIFY(proxyAfterSpy.isValid());
{
ModelResetCommandFixed resetCommand(&model, 0);
resetCommand.setAncestorRowNumbers(QList<int>() << 5);
resetCommand.setStartRow(3);
resetCommand.setEndRow(4);
resetCommand.setDestRow(1);
resetCommand.doCommand();
}
QVERIFY(modelBeforeSpy.size() == 1 && 1 == proxyBeforeSpy.size());
QVERIFY(modelAfterSpy.size() == 1 && 1 == proxyAfterSpy.size());
verifyIdentity(&model);
m_proxy->setSourceModel(0);
}
void tst_QIdentityProxyModel::dataChanged()
{
DataChangedModel model;
m_proxy->setSourceModel(&model);
verifyIdentity(&model);
QSignalSpy modelSpy(&model, &DataChangedModel::dataChanged);
QSignalSpy proxySpy(m_proxy, &QIdentityProxyModel::dataChanged);
QVERIFY(modelSpy.isValid());
QVERIFY(proxySpy.isValid());
model.changeData();
QCOMPARE(modelSpy.first().at(2).value<QList<int> >(), QList<int>() << 1);
QCOMPARE(modelSpy.first().at(2), proxySpy.first().at(2));
verifyIdentity(&model);
m_proxy->setSourceModel(0);
}
class AppendStringProxy : public QIdentityProxyModel
{
public:
QVariant data(const QModelIndex &index, int role) const override
{
const QVariant result = QIdentityProxyModel::data(index, role);
if (role != Qt::DisplayRole)
return result;
return result.toString() + QLatin1String("_appended");
}
QMap<int, QVariant> itemData(const QModelIndex &index) const override
{
QMap<int, QVariant> result = QIdentityProxyModel::itemData(index);
auto displayIter = result.find(Qt::DisplayRole);
if (displayIter != result.end())
displayIter.value() = displayIter.value().toString() + QLatin1String("_appended");
return result;
}
};
void tst_QIdentityProxyModel::itemData()
{
QStringListModel model(QStringList() << "Monday" << "Tuesday" << "Wednesday");
AppendStringProxy proxy;
proxy.setSourceModel(&model);
const QModelIndex topIndex = proxy.index(0, 0);
QCOMPARE(topIndex.data(Qt::DisplayRole).toString(), QStringLiteral("Monday_appended"));
QCOMPARE(proxy.data(topIndex, Qt::DisplayRole).toString(), QStringLiteral("Monday_appended"));
QCOMPARE(proxy.itemData(topIndex).value(Qt::DisplayRole).toString(), QStringLiteral("Monday_appended"));
}
void dump(QAbstractItemModel* model, QString const& indent = " - ", QModelIndex const& parent = {})
{
for (auto row = 0; row < model->rowCount(parent); ++row)
{
auto idx = model->index(row, 0, parent);
qCDebug(lcItemModels) << (indent + idx.data().toString());
dump(model, indent + "- ", idx);
}
}
void tst_QIdentityProxyModel::persistIndexOnLayoutChange()
{
DynamicTreeModel model;
QList<int> ancestors;
for (auto i = 0; i < 3; ++i)
{
Q_UNUSED(i);
ModelInsertCommand insertCommand(&model);
insertCommand.setAncestorRowNumbers(ancestors);
insertCommand.setStartRow(0);
insertCommand.setEndRow(0);
insertCommand.doCommand();
ancestors.push_back(0);
}
ModelInsertCommand insertCommand(&model);
insertCommand.setAncestorRowNumbers(ancestors);
insertCommand.setStartRow(0);
insertCommand.setEndRow(1);
insertCommand.doCommand();
// dump(&model);
// " - 1"
// " - - 2"
// " - - - 3"
// " - - - - 4"
// " - - - - 5"
QIdentityProxyModel proxy;
proxy.setSourceModel(&model);
QPersistentModelIndex persistentIndex;
QPersistentModelIndex sourcePersistentIndex = model.match(model.index(0, 0), Qt::DisplayRole, "5", 1, Qt::MatchRecursive).first();
QCOMPARE(sourcePersistentIndex.data().toString(), QStringLiteral("5"));
bool gotLayoutAboutToBeChanged = false;
bool gotLayoutChanged = false;
QObject::connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, &proxy, [&proxy, &persistentIndex, &gotLayoutAboutToBeChanged]
{
gotLayoutAboutToBeChanged = true;
persistentIndex = proxy.match(proxy.index(0, 0), Qt::DisplayRole, "5", 1, Qt::MatchRecursive).first();
});
QObject::connect(&proxy, &QAbstractItemModel::layoutChanged, &proxy, [&proxy, &persistentIndex, &sourcePersistentIndex, &gotLayoutChanged]
{
gotLayoutChanged = true;
QCOMPARE(QModelIndex(persistentIndex), proxy.mapFromSource(sourcePersistentIndex));
});
ModelChangeChildrenLayoutsCommand layoutChangeCommand(&model, 0);
layoutChangeCommand.setAncestorRowNumbers(QList<int>{0, 0, 0});
layoutChangeCommand.setSecondAncestorRowNumbers(QList<int>{0, 0});
layoutChangeCommand.doCommand();
QVERIFY(gotLayoutAboutToBeChanged);
QVERIFY(gotLayoutChanged);
QVERIFY(persistentIndex.isValid());
}
void tst_QIdentityProxyModel::createPersistentOnLayoutAboutToBeChanged() // QTBUG-93466
{
QStandardItemModel model(3, 1);
for (int row = 0; row < 3; ++row)
model.setData(model.index(row, 0), row, Qt::UserRole);
model.setSortRole(Qt::UserRole);
QIdentityProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QList<QPersistentModelIndex> idxList;
QSignalSpy layoutAboutToBeChangedSpy(&proxy, &QAbstractItemModel::layoutAboutToBeChanged);
QSignalSpy layoutChangedSpy(&proxy, &QAbstractItemModel::layoutChanged);
connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, this, [&idxList, &proxy](){
idxList.clear();
for (int row = 0; row < 3; ++row)
idxList << QPersistentModelIndex(proxy.index(row, 0));
});
connect(&proxy, &QAbstractItemModel::layoutChanged, this, [&idxList](){
QCOMPARE(idxList.size(), 3);
QCOMPARE(idxList.at(0).row(), 1);
QCOMPARE(idxList.at(0).column(), 0);
QCOMPARE(idxList.at(0).data(Qt::UserRole).toInt(), 0);
QCOMPARE(idxList.at(1).row(), 0);
QCOMPARE(idxList.at(1).column(), 0);
QCOMPARE(idxList.at(1).data(Qt::UserRole).toInt(), -1);
QCOMPARE(idxList.at(2).row(), 2);
QCOMPARE(idxList.at(2).column(), 0);
QCOMPARE(idxList.at(2).data(Qt::UserRole).toInt(), 2);
});
model.setData(model.index(1, 0), -1, Qt::UserRole);
model.sort(0);
QCOMPARE(layoutAboutToBeChangedSpy.size(), 1);
QCOMPARE(layoutChangedSpy.size(), 1);
}
QTEST_MAIN(tst_QIdentityProxyModel)
#include "tst_qidentityproxymodel.moc"

View File

@ -0,0 +1,15 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qitemmodel Test:
#####################################################################
qt_internal_add_test(tst_qitemmodel
SOURCES
tst_qitemmodel.cpp
LIBRARIES
Qt::Gui
Qt::Sql
Qt::Widgets
)

View File

@ -0,0 +1,3 @@
This is a QStandardItemModel test. It will help catch a lot of simple problems. You should still create your own test for custom functionality and functions that your model has.
Add your model to the modelstotest.cpp file (qt model's are included as examples) and modify the pro file accordingly. Fix the errors in order of failure as later tests assume the ones before them have passed.

View File

@ -0,0 +1,328 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QtCore/QCoreApplication>
#include <QtSql/QtSql>
#include <QtWidgets/QtWidgets>
#include <QtCore/QSortFilterProxyModel>
/*
To add a model to be tested add the header file to the includes
and impliment what is needed in the four functions below.
You can add more then one model, several Qt models and included as examples.
In tst_qitemmodel.cpp a new ModelsToTest object is created for each test.
When you have errors fix the first ones first. Later tests depend upon them working
*/
class ModelsToTest {
public:
ModelsToTest();
QAbstractItemModel *createModel(const QString &modelType);
QModelIndex populateTestArea(QAbstractItemModel *model);
void cleanupTestArea(QAbstractItemModel *model);
enum Read {
ReadOnly, // won't perform remove(), insert(), and setData()
ReadWrite
};
enum Contains {
Empty, // Confirm that rowCount() == 0 etc throughout the test
HasData // Confirm that rowCount() != 0 etc throughout the test
};
struct test {
test(QString m, Read r, Contains c) : modelType(m), read(r), contains(c){};
QString modelType;
Read read;
Contains contains;
};
QList<test> tests;
static void setupDatabase();
private:
QScopedPointer<QTemporaryDir> m_dirModelTempDir;
};
/*!
Add new tests, they can be the same model, but in a different state.
The name of the model is passed to createModel
If readOnly is true the remove tests will be skipped. Example: QSqlQueryModel is disabled.
If createModel returns an empty model.
*/
ModelsToTest::ModelsToTest()
{
setupDatabase();
tests.append(test("QStringListModel", ReadWrite, HasData));
tests.append(test("QStringListModelEmpty", ReadWrite, Empty));
tests.append(test("QStandardItemModel", ReadWrite, HasData));
tests.append(test("QStandardItemModelEmpty", ReadWrite, Empty));
// QSortFilterProxyModel test uses QStandardItemModel so test it first
tests.append(test("QSortFilterProxyModel", ReadWrite, HasData));
tests.append(test("QSortFilterProxyModelEmpty", ReadWrite, Empty));
tests.append(test("QSortFilterProxyModelRegExp", ReadWrite, HasData));
tests.append(test("QListModel", ReadWrite, HasData));
tests.append(test("QListModelEmpty", ReadWrite, Empty));
tests.append(test("QTableModel", ReadWrite, HasData));
tests.append(test("QTableModelEmpty", ReadWrite, Empty));
tests.append(test("QTreeModel", ReadWrite, HasData));
tests.append(test("QTreeModelEmpty", ReadWrite, Empty));
tests.append(test("QSqlQueryModel", ReadOnly, HasData));
tests.append(test("QSqlQueryModelEmpty", ReadOnly, Empty));
// Fails on remove
tests.append(test("QSqlTableModel", ReadOnly, HasData));
}
/*!
Return a new modelType.
*/
QAbstractItemModel *ModelsToTest::createModel(const QString &modelType)
{
if (modelType == "QStringListModelEmpty")
return new QStringListModel();
if (modelType == "QStringListModel") {
QStringListModel *model = new QStringListModel();
populateTestArea(model);
return model;
}
if (modelType == "QStandardItemModelEmpty") {
return new QStandardItemModel();
}
if (modelType == "QStandardItemModel") {
QStandardItemModel *model = new QStandardItemModel();
populateTestArea(model);
return model;
}
if (modelType == "QSortFilterProxyModelEmpty") {
QSortFilterProxyModel *model = new QSortFilterProxyModel;
QStandardItemModel *standardItemModel = new QStandardItemModel(model);
model->setSourceModel(standardItemModel);
return model;
}
if (modelType == "QSortFilterProxyModelRegExp") {
QSortFilterProxyModel *model = new QSortFilterProxyModel;
QStandardItemModel *standardItemModel = new QStandardItemModel(model);
model->setSourceModel(standardItemModel);
populateTestArea(model);
model->setFilterRegularExpression(QRegularExpression("(^$|I.*)"));
return model;
}
if (modelType == "QSortFilterProxyModel") {
QSortFilterProxyModel *model = new QSortFilterProxyModel;
QStandardItemModel *standardItemModel = new QStandardItemModel(model);
model->setSourceModel(standardItemModel);
populateTestArea(model);
return model;
}
if (modelType == "QSqlQueryModel") {
QSqlQueryModel *model = new QSqlQueryModel();
populateTestArea(model);
return model;
}
if (modelType == "QSqlQueryModelEmpty") {
QSqlQueryModel *model = new QSqlQueryModel();
return model;
}
if (modelType == "QSqlTableModel") {
QSqlTableModel *model = new QSqlTableModel();
populateTestArea(model);
return model;
}
if (modelType == "QListModelEmpty")
return (new QListWidget)->model();
if (modelType == "QListModel") {
QListWidget *widget = new QListWidget;
populateTestArea(widget->model());
return widget->model();
}
if (modelType == "QTableModelEmpty")
return (new QTableWidget)->model();
if (modelType == "QTableModel") {
QTableWidget *widget = new QTableWidget;
populateTestArea(widget->model());
return widget->model();
}
if (modelType == "QTreeModelEmpty") {
QTreeWidget *widget = new QTreeWidget;
return widget->model();
}
if (modelType == "QTreeModel") {
QTreeWidget *widget = new QTreeWidget;
populateTestArea(widget->model());
return widget->model();
}
return 0;
}
/*!
Fills model with some test data at least twenty rows and if possible twenty or more columns.
Return the parent of a row/column that can be tested.
NOTE: If readOnly is true tests will try to remove and add rows and columns.
If you have a tree model returning not the root index will help catch more errors.
*/
QModelIndex ModelsToTest::populateTestArea(QAbstractItemModel *model)
{
if (QStringListModel *stringListModel = qobject_cast<QStringListModel *>(model)) {
QString alphabet = "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z";
stringListModel->setStringList( alphabet.split(QLatin1Char(',')) );
return QModelIndex();
}
if (qobject_cast<QStandardItemModel *>(model)) {
// Basic tree StandardItemModel
QModelIndex parent;
QVariant blue = QVariant(QColor(Qt::blue));
for (int i = 0; i < 4; ++i) {
parent = model->index(0, 0, parent);
model->insertRows(0, 26 + i, parent);
model->insertColumns(0, 4 + i, parent);
// Fill in some values to make it easier to debug
/*
for (int x = 0; x < 26 + i; ++x) {
QString xval = QString::number(x);
for (int y = 0; y < 26 + i; ++y) {
QString val = xval + QString::number(y) + QString::number(i);
QModelIndex index = model->index(x, y, parent);
model->setData(index, val);
model->setData(index, blue, Qt::ForegroundRole);
}
}
*/
}
return model->index(0,0);
}
if (qobject_cast<QSortFilterProxyModel *>(model)) {
QAbstractItemModel *realModel = (qobject_cast<QSortFilterProxyModel *>(model))->sourceModel();
// Basic tree StandardItemModel
QModelIndex parent;
QVariant blue = QVariant(QColor(Qt::blue));
for (int i = 0; i < 4; ++i) {
parent = realModel->index(0, 0, parent);
realModel->insertRows(0, 26+i, parent);
realModel->insertColumns(0, 4, parent);
// Fill in some values to make it easier to debug
/*
for (int x = 0; x < 26+i; ++x) {
QString xval = QString::number(x);
for (int y = 0; y < 26 + i; ++y) {
QString val = xval + QString::number(y) + QString::number(i);
QModelIndex index = realModel->index(x, y, parent);
realModel->setData(index, val);
realModel->setData(index, blue, Qt::ForegroundRole);
}
}
*/
}
QModelIndex returnIndex = model->index(0,0);
if (!returnIndex.isValid())
qFatal("%s: model index to be returned is invalid", Q_FUNC_INFO);
return returnIndex;
}
if (QSqlQueryModel *queryModel = qobject_cast<QSqlQueryModel *>(model)) {
QSqlQuery q;
q.exec("CREATE TABLE test(id int primary key, name varchar(30))");
q.prepare("INSERT INTO test(id, name) values (?, ?)");
for (int i = 0; i < 1024; ++i) {
q.addBindValue(i);
q.addBindValue("Mr. Smith" + QString::number(i));
q.exec();
}
if (QSqlTableModel *tableModel = qobject_cast<QSqlTableModel *>(model)) {
tableModel->setTable("test");
tableModel->setEditStrategy(QSqlTableModel::OnManualSubmit);
tableModel->select();
} else {
queryModel->setQuery("select * from test");
}
return QModelIndex();
}
if (QListWidget *listWidget = qobject_cast<QListWidget *>(model->parent())) {
int items = 50;
while (items--)
listWidget->addItem(QLatin1String("item ") + QString::number(items));
return QModelIndex();
}
if (QTableWidget *tableWidget = qobject_cast<QTableWidget *>(model->parent())) {
tableWidget->setColumnCount(20);
tableWidget->setRowCount(20);
return QModelIndex();
}
if (QTreeWidget *treeWidget = qobject_cast<QTreeWidget *>(model->parent())) {
int topItems = 20;
treeWidget->setColumnCount(1);
QTreeWidgetItem *parent;
while (topItems--){
const QString tS = QString::number(topItems);
parent = new QTreeWidgetItem(treeWidget, QStringList(QLatin1String("top ") + tS));
for (int i = 0; i < 20; ++i)
new QTreeWidgetItem(parent, QStringList(QLatin1String("child ") + tS));
}
return QModelIndex();
}
qFatal("%s: unknown type of model", Q_FUNC_INFO);
return QModelIndex();
}
/*!
If you need to cleanup from populateTest() do it here.
Note that this is called after every test even if populateTestArea isn't called.
*/
void ModelsToTest::cleanupTestArea(QAbstractItemModel *model)
{
if (qobject_cast<QSqlQueryModel *>(model))
QSqlQuery q("DROP TABLE test");
}
void ModelsToTest::setupDatabase()
{
if (!QSqlDatabase::database().isValid()) {
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(":memory:");
if (!db.open()) {
qWarning() << "Unable to open database" << db.lastError();
return;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qitemselectionmodel Test:
#####################################################################
qt_internal_add_test(tst_qitemselectionmodel
SOURCES
tst_qitemselectionmodel.cpp
LIBRARIES
Qt::Gui
Qt::TestPrivate
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qsortfilterproxymodel Test:
#####################################################################
qt_internal_add_test(tst_qsortfilterproxymodel
SOURCES
../../../other/qabstractitemmodelutils/dynamictreemodel.cpp ../../../other/qabstractitemmodelutils/dynamictreemodel.h
tst_qsortfilterproxymodel.cpp
INCLUDE_DIRECTORIES
../../../other/qabstractitemmodelutils
LIBRARIES
Qt::Gui
Qt::Widgets
Qt::TestPrivate
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#ifndef TST_QSORTFILTERPROXYMODEL_H
#define TST_QSORTFILTERPROXYMODEL_H
#include "dynamictreemodel.h"
#include <QLoggingCategory>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
enum class FilterType {
RegExp,
RegularExpression
};
Q_DECLARE_METATYPE(QList<QPersistentModelIndex>)
class tst_QSortFilterProxyModel : public QObject
{
Q_OBJECT
public slots:
void initTestCase();
void cleanupTestCase();
void cleanup();
private slots:
void getSetCheck();
void sort_data();
void sort();
void sortHierarchy_data();
void sortHierarchy();
void createPersistentOnLayoutAboutToBeChanged();
void insertRows_data();
void insertRows();
void prependRow();
void appendRowFromCombobox_data();
void appendRowFromCombobox();
void removeRows_data();
void removeRows();
void removeColumns_data();
void removeColumns();
void insertAfterSelect();
void removeAfterSelect();
void filter_data();
void filter();
void filterHierarchy_data();
void filterHierarchy();
void filterColumns_data();
void filterColumns();
void filterTable();
void filterCurrent();
void filter_qtbug30662();
void changeSourceLayout();
void changeSourceLayoutFilteredOut();
void removeSourceRows_data();
void removeSourceRows();
void insertSourceRows_data();
void insertSourceRows();
void changeFilter_data();
void changeFilter();
void changeSourceData_data();
void changeSourceData();
void changeSourceDataKeepsStableSorting_qtbug1548();
void changeSourceDataForwardsRoles_qtbug35440();
void changeSourceDataProxySendDataChanged_qtbug87781();
void changeSourceDataTreeModel();
void changeSourceDataProxyFilterSingleColumn();
void changeSourceDataProxyFilterMultipleColumns();
void resortingDoesNotBreakTreeModels();
void dynamicFilterWithoutSort();
void sortFilterRole();
void selectionFilteredOut();
void match_data();
void match();
void matchTree();
void insertIntoChildrenlessItem();
void invalidateMappedChildren();
void insertRowIntoFilteredParent();
void filterOutParentAndFilterInChild();
void sourceInsertRows();
void sourceModelDeletion();
void sortColumnTracking1();
void sortColumnTracking2();
void sortStable();
void hiddenColumns();
void insertRowsSort();
void staticSorting();
void dynamicSorting();
void fetchMore();
void hiddenChildren();
void mapFromToSource();
void removeRowsRecursive();
void doubleProxySelectionSetSourceModel();
void appearsAndSort();
void unnecessaryDynamicSorting();
void unnecessaryMapCreation();
void resetInvalidate_data();
void resetInvalidate();
void testMultipleProxiesWithSelection();
void mapSelectionFromSource();
void testResetInternalData();
void filteredColumns();
void headerDataChanged();
void testParentLayoutChanged();
void moveSourceRows();
void hierarchyFilterInvalidation();
void simpleFilterInvalidation();
void chainedProxyModelRoleNames();
void noMapAfterSourceDelete();
void forwardDropApi();
void canDropMimeData();
void filterHint();
void sourceLayoutChangeLeavesValidPersistentIndexes();
void rowMoveLeavesValidPersistentIndexes();
void emitLayoutChangedOnlyIfSortingChanged_data();
void emitLayoutChangedOnlyIfSortingChanged();
void checkSetNewModel();
void filterAndInsertRow_data();
void filterAndInsertRow();
void filterAndInsertColumn_data();
void filterAndInsertColumn();
void removeIntervals_data();
void removeIntervals();
void checkFilteredIndexes();
void invalidateColumnsOrRowsFilter();
void filterKeyColumnBinding();
void dynamicSortFilterBinding();
void sortCaseSensitivityBinding();
void isSortLocaleAwareBinding();
void sortRoleBinding();
void filterRoleBinding();
void recursiveFilteringEnabledBinding();
void autoAcceptChildRowsBinding();
void filterCaseSensitivityBinding();
void filterRegularExpressionBinding();
protected:
void buildHierarchy(const QStringList &data, QAbstractItemModel *model);
void checkHierarchy(const QStringList &data, const QAbstractItemModel *model);
void setupFilter(QSortFilterProxyModel *model, const QString& pattern);
protected:
FilterType m_filterType;
private:
QStandardItemModel *m_model = nullptr;
QSortFilterProxyModel *m_proxy = nullptr;
};
Q_DECLARE_METATYPE(QAbstractItemModel::LayoutChangeHint)
Q_DECLARE_LOGGING_CATEGORY(lcItemModels)
#endif // TST_QSORTFILTERPROXYMODEL_H

View File

@ -0,0 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qsortfilterproxymodel_recursive Test:
#####################################################################
qt_internal_add_test(tst_qsfpm_recursive
SOURCES
tst_qsortfilterproxymodel_recursive.cpp
LIBRARIES
Qt::Gui
)

View File

@ -0,0 +1,733 @@
// Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, authors Filipe Azevedo <filipe.azevedo@kdab.com> and David Faure <david.faure@kdab.com>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QSignalSpy>
#include <QSortFilterProxyModel>
#include <QStandardItem>
Q_DECLARE_METATYPE(QModelIndex)
static const int s_filterRole = Qt::UserRole + 1;
class ModelSignalSpy : public QObject {
Q_OBJECT
public:
explicit ModelSignalSpy(QAbstractItemModel &model) {
connect(&model, &QAbstractItemModel::rowsInserted, this, &ModelSignalSpy::onRowsInserted);
connect(&model, &QAbstractItemModel::rowsRemoved, this, &ModelSignalSpy::onRowsRemoved);
connect(&model, &QAbstractItemModel::rowsAboutToBeInserted, this, &ModelSignalSpy::onRowsAboutToBeInserted);
connect(&model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelSignalSpy::onRowsAboutToBeRemoved);
connect(&model, &QAbstractItemModel::rowsMoved, this, &ModelSignalSpy::onRowsMoved);
connect(&model, &QAbstractItemModel::dataChanged, this, &ModelSignalSpy::onDataChanged);
connect(&model, &QAbstractItemModel::layoutChanged, this, &ModelSignalSpy::onLayoutChanged);
connect(&model, &QAbstractItemModel::modelReset, this, &ModelSignalSpy::onModelReset);
}
QStringList mSignals;
private Q_SLOTS:
void onRowsInserted(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsInserted(") + textForRowSpy(p, start, end) + ')';
}
void onRowsRemoved(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsRemoved(") + textForRowSpy(p, start, end) + ')';
}
void onRowsAboutToBeInserted(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsAboutToBeInserted(") + textForRowSpy(p, start, end) + ')';
}
void onRowsAboutToBeRemoved(QModelIndex p, int start, int end) {
mSignals << QLatin1String("rowsAboutToBeRemoved(") + textForRowSpy(p, start, end) + ')';
}
void onRowsMoved(QModelIndex,int,int,QModelIndex,int) {
mSignals << QStringLiteral("rowsMoved");
}
void onDataChanged(const QModelIndex &from, const QModelIndex& ) {
mSignals << QStringLiteral("dataChanged(%1)").arg(from.data(Qt::DisplayRole).toString());
}
void onLayoutChanged() {
mSignals << QStringLiteral("layoutChanged");
}
void onModelReset() {
mSignals << QStringLiteral("modelReset");
}
private:
QString textForRowSpy(const QModelIndex &parent, int start, int end)
{
QString txt = parent.data(Qt::DisplayRole).toString();
if (!txt.isEmpty())
txt += QLatin1Char('.');
txt += QString::number(start+1);
if (start != end)
txt += QLatin1Char('-') + QString::number(end+1);
return txt;
}
};
class TestModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
TestModel(QAbstractItemModel *sourceModel)
: QSortFilterProxyModel()
{
setFilterRole(s_filterRole);
setRecursiveFilteringEnabled(true);
setSourceModel(sourceModel);
}
virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
return sourceModel()->index(sourceRow, 0, sourceParent).data(s_filterRole).toBool()
&& QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
}
};
// Represents this tree
// - A
// - - B
// - - - C
// - - - D
// - - E
// as a single string, englobing children in brackets, like this:
// [A[B[C D] E]]
// In addition, items that match the filtering (data(s_filterRole) == true) have a * after their value.
static QString treeAsString(const QAbstractItemModel &model, const QModelIndex &parent = QModelIndex())
{
QString ret;
const int rowCount = model.rowCount(parent);
if (rowCount > 0) {
ret += QLatin1Char('[');
for (int row = 0 ; row < rowCount; ++row) {
if (row > 0) {
ret += ' ';
}
const QModelIndex child = model.index(row, 0, parent);
ret += child.data(Qt::DisplayRole).toString();
if (child.data(s_filterRole).toBool())
ret += QLatin1Char('*');
ret += treeAsString(model, child);
}
ret += QLatin1Char(']');
}
return ret;
}
// Fill a tree model based on a string representation (see treeAsString)
static void fillModel(QStandardItemModel &model, const QString &str)
{
QCOMPARE(str.count('['), str.count(']'));
QStandardItem *item = nullptr;
QString data;
for ( int i = 0 ; i < str.size() ; ++i ) {
const QChar ch = str.at(i);
if ((ch == '[' || ch == ']' || ch == ' ') && !data.isEmpty()) {
if (data.endsWith('*')) {
item->setData(true, s_filterRole);
data.chop(1);
}
item->setText(data);
data.clear();
}
if (ch == '[') {
// Create new child
QStandardItem *child = new QStandardItem;
if (item)
item->appendRow(child);
else
model.appendRow(child);
item = child;
} else if (ch == ']') {
// Go up to parent
item = item->parent();
} else if (ch == ' ') {
// Create new sibling
QStandardItem *child = new QStandardItem;
QStandardItem *parent = item->parent();
if (parent)
parent->appendRow(child);
else
model.appendRow(child);
item = child;
} else {
data += ch;
}
}
}
class tst_QSortFilterProxyModel_Recursive : public QObject
{
Q_OBJECT
private:
private Q_SLOTS:
void testInitialFiltering_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("proxyStr");
QTest::newRow("empty") << "[]" << "";
QTest::newRow("no") << "[1]" << "";
QTest::newRow("yes") << "[1*]" << "[1*]";
QTest::newRow("second") << "[1 2*]" << "[2*]";
QTest::newRow("child_yes") << "[1 2[2.1*]]" << "[2[2.1*]]";
QTest::newRow("grandchild_yes") << "[1 2[2.1[2.1.1*]]]" << "[2[2.1[2.1.1*]]]";
// 1, 3.1 and 4.2.1 match, so their parents are in the model
QTest::newRow("more") << "[1* 2[2.1] 3[3.1*] 4[4.1 4.2[4.2.1*]]]" << "[1* 3[3.1*] 4[4.2[4.2.1*]]]";
}
void testInitialFiltering()
{
QFETCH(QString, sourceStr);
QFETCH(QString, proxyStr);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QVERIFY(proxy.isRecursiveFilteringEnabled());
QCOMPARE(treeAsString(proxy), proxyStr);
}
// Test changing a role that is unrelated to the filtering.
void testUnrelatedDataChange()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
// When changing the text on the item
item_1_1_1->setText(QStringLiteral("ME"));
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[ME*]]]"));
// filterRole is Qt::UserRole + 1, so parents are not checked and
// therefore no dataChanged for parents
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("dataChanged(ME)"));
}
// Test changing a role that is unrelated to the filtering, in a hidden item.
void testHiddenDataChange()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
// When changing the text on a hidden item
item_1_1_1->setText(QStringLiteral("ME"));
QCOMPARE(treeAsString(proxy), QString());
QCOMPARE(spy.mSignals, QStringList());
}
// Test that we properly react to a data-changed signal in a descendant and include all required rows
void testDataChangeIn_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("add"); // set the flag on this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
QTest::newRow("toplevel") << "[1]" << "" << "1" << "[1*]"
<< (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
QTest::newRow("show_parents") << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "[1[1.1[1.1.1*]]]"
<< (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)") << QStringLiteral("rowsInserted(1)"));
const QStringList insert_1_1_1 = QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.1)")
<< QStringLiteral("rowsInserted(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)")
;
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*[1.1.1*]]]"
<< insert_1_1_1;
QTest::newRow("sibling_visible") << "[1[1.1[1.1.1 1.1.2*]]]" << "[1[1.1[1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2*]]]"
<< insert_1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"
<< insert_1_1_1;
QTest::newRow("show_parent") << "[1[1.1[1.1.1 1.1.2] 1.2*]]" << "[1[1.2*]]" << "1.1.1" << "[1[1.1[1.1.1*] 1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1)")
<< QStringLiteral("rowsInserted(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]"
<< (QStringList()
<< QStringLiteral("dataChanged(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
}
void testDataChangeIn()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, add);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
// When changing the data on the designated item to show this row
QStandardItem *itemToChange = itemByText(model, add);
QVERIFY(!itemToChange->data(s_filterRole).toBool());
itemToChange->setData(true, s_filterRole);
// The proxy should update as expected
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testDataChangeOut_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("remove"); // unset the flag on this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
const QStringList remove1_1_1 = (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("toplevel") << "[1*]" << "[1*]" << "1" << ""
<< (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
QTest::newRow("hide_parent") << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << "" <<
(QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]"
<< remove1_1_1;
QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]"
<< remove1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]"
<< remove1_1_1;
// The following tests trigger the removal of an ascendant.
QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]"
<< (QStringList()
<< QStringLiteral("dataChanged(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << ""
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
}
void testDataChangeOut()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, remove);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
// When changing the data on the designated item to exclude this row again
QStandardItem *itemToChange = itemByText(model, remove);
QVERIFY(itemToChange->data(s_filterRole).toBool());
itemToChange->setData(false, s_filterRole);
// The proxy should update as expected
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testInsert()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true, s_filterRole);
item_1_1_1->appendRow(item_1_1_1_1);
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1)")
<< QStringLiteral("rowsInserted(1)"));
}
// Start from [1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]
// where 1.1.1 is hidden but 1.1 is shown, we want to insert a shown child in 1.1.1.
// The proxy ensures dataChanged is called on 1.1,
// so that 1.1.1 and 1.1.1.1 are included in the model.
void testInsertCousin()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.2[1.1.2.1*]]]]"));
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true, s_filterRole);
QStandardItem *item_1_1_1 = model.item(0)->child(0)->child(0);
item_1_1_1->appendRow(item_1_1_1_1);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*] 1.1.2[1.1.2.1*]]]]"));
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.1)")
<< QStringLiteral("rowsInserted(1.1.1)")
<< QStringLiteral("dataChanged(1.1)")
<< QStringLiteral("dataChanged(1)"));
}
void testInsertWithChildren()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1_1->setData(true, s_filterRole);
item_1_1_1->appendRow(item_1_1_1_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1)")
<< QStringLiteral("rowsInserted(1)"));
}
void testInsertIntoVisibleWithChildren()
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_2 = new QStandardItem(QStringLiteral("1.1.2"));
QStandardItem *item_1_1_2_1 = new QStandardItem(QStringLiteral("1.1.2.1"));
item_1_1_2_1->setData(true, s_filterRole);
item_1_1_2->appendRow(item_1_1_2_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_2);
}
QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]"));
QCOMPARE(spy.mSignals, QStringList()
<< QStringLiteral("rowsAboutToBeInserted(1.1.2)")
<< QStringLiteral("rowsInserted(1.1.2)"));
}
void testInsertBefore()
{
QStandardItemModel model;
const QString sourceStr = "[1[1.1[1.1.2*]]]";
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), sourceStr);
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem("1.1.1");
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->insertRow(0, item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QString("[1[1.1[1.1.2*]]]"));
QCOMPARE(spy.mSignals, QStringList());
}
void testInsertHidden() // inserting filtered-out rows shouldn't emit anything
{
QStandardItemModel model;
const QString sourceStr = QStringLiteral("[1[1.1]]");
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), QString());
ModelSignalSpy spy(proxy);
{
QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1"));
QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1"));
item_1_1_1->appendRow(item_1_1_1_1);
QStandardItem *item_1_1 = model.item(0)->child(0);
item_1_1->appendRow(item_1_1_1);
}
QCOMPARE(treeAsString(proxy), QString());
QCOMPARE(spy.mSignals, QStringList());
}
void testConsecutiveInserts_data()
{
testInitialFiltering_data();
}
void testConsecutiveInserts()
{
QFETCH(QString, sourceStr);
QFETCH(QString, proxyStr);
QStandardItemModel model;
TestModel proxy(&model); // this time the proxy listens to the model while we fill it
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
QCOMPARE(treeAsString(proxy), proxyStr);
}
void testRemove_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("remove"); // remove this item
QTest::addColumn<QString>("expectedProxyStr");
QTest::addColumn<QStringList>("expectedSignals");
const QStringList remove1_1_1 = (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)") << QStringLiteral("rowsRemoved(1.1.1)"));
QTest::newRow("toplevel") << "[1* 2* 3*]" << "[1* 2* 3*]" << "1" << "[2* 3*]"
<< (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)") << QStringLiteral("rowsRemoved(1)"));
QTest::newRow("remove_hidden") << "[1 2* 3*]" << "[2* 3*]" << "1" << "[2* 3*]" << QStringList();
QTest::newRow("parent_hidden") << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "" << QStringList();
QTest::newRow("child_hidden") << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*]]" << QStringList();
QTest::newRow("parent_visible") << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]"
<< remove1_1_1;
QTest::newRow("visible") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]"
<< remove1_1_1;
QTest::newRow("visible_cousin") << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]"
<< remove1_1_1;
// The following tests trigger the removal of an ascendant.
// We could optimize the rows{AboutToBe,}Removed(1.1.1) away...
QTest::newRow("remove_parent") << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1.1)")
<< QStringLiteral("rowsRemoved(1.1)")
<< QStringLiteral("dataChanged(1)"));
QTest::newRow("with_children") << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[2*]"
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
QTest::newRow("last_visible") << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << ""
<< (QStringList()
<< QStringLiteral("rowsAboutToBeRemoved(1.1.1)")
<< QStringLiteral("rowsRemoved(1.1.1)")
<< QStringLiteral("rowsAboutToBeRemoved(1)")
<< QStringLiteral("rowsRemoved(1)"));
}
void testRemove()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, remove);
QFETCH(QString, expectedProxyStr);
QFETCH(QStringList, expectedSignals);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
QStandardItem *itemToRemove = itemByText(model, remove);
QVERIFY(itemToRemove);
if (itemToRemove->parent())
itemToRemove->parent()->removeRow(itemToRemove->row());
else
model.removeRow(itemToRemove->row());
QCOMPARE(treeAsString(proxy), expectedProxyStr);
//qDebug() << spy.mSignals;
QCOMPARE(spy.mSignals, expectedSignals);
}
void testStandardFiltering_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("initialProxyStr");
QTest::addColumn<QString>("filter");
QTest::addColumn<QString>("expectedProxyStr");
QTest::newRow("select_child") << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]"
<< "1.1.2" << "[1[1.1[1.1.2*]]]";
QTest::newRow("filter_all_out") << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]"
<< "test" << "";
QTest::newRow("select_parent") << "[1[1.1[1.1.1*[child*] 1.1.2*]]]" << "[1[1.1[1.1.1*[child*] 1.1.2*]]]"
<< "1.1.1" << "[1[1.1[1.1.1*]]]";
}
void testStandardFiltering()
{
QFETCH(QString, sourceStr);
QFETCH(QString, initialProxyStr);
QFETCH(QString, filter);
QFETCH(QString, expectedProxyStr);
QStandardItemModel model;
fillModel(model, sourceStr);
QCOMPARE(treeAsString(model), sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), initialProxyStr);
ModelSignalSpy spy(proxy);
//qDebug() << "setFilterFixedString";
proxy.setFilterRole(Qt::DisplayRole);
proxy.setFilterFixedString(filter);
QCOMPARE(treeAsString(proxy), expectedProxyStr);
}
void testChildrenFiltering_data()
{
QTest::addColumn<QString>("sourceStr");
QTest::addColumn<QString>("noChildrenProxyStr");
QTest::addColumn<QString>("childrenProxyStr");
QTest::addColumn<QString>("noParentProxyStr");
QTest::newRow("filter_parent") << "[1*[1.1 1.2[1.2.1]]]" << "[1*]" << "[1*[1.1 1.2[1.2.1]]]" << "[1*[1.1 1.2[1.2.1]]]";
QTest::newRow("filter_child") << "[1[1.1 1.2*[1.2.1]]]" << "[1[1.2*]]" << "[1[1.2*[1.2.1]]]" << "";
}
void testChildrenFiltering()
{
QFETCH(QString, sourceStr);
QFETCH(QString, noChildrenProxyStr);
QFETCH(QString, childrenProxyStr);
QFETCH(QString, noParentProxyStr);
QStandardItemModel model;
fillModel(model, sourceStr);
TestModel proxy(&model);
QCOMPARE(treeAsString(proxy), noChildrenProxyStr);
proxy.setAutoAcceptChildRows(true);
QCOMPARE(treeAsString(proxy), childrenProxyStr);
proxy.setRecursiveFilteringEnabled(false);
QCOMPARE(treeAsString(proxy), noParentProxyStr);
}
private:
QStandardItem *itemByText(const QStandardItemModel& model, const QString &text) const {
QModelIndexList list = model.match(model.index(0, 0), Qt::DisplayRole, text, 1, Qt::MatchRecursive);
return list.isEmpty() ? 0 : model.itemFromIndex(list.first());
}
};
QTEST_GUILESS_MAIN(tst_QSortFilterProxyModel_Recursive)
#include "tst_qsortfilterproxymodel_recursive.moc"

View File

@ -0,0 +1,15 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qsortfilterproxymodel_regularexpression Test:
#####################################################################
qt_internal_add_test(tst_qsfpm_regex
SOURCES
tst_qsortfilterproxymodel_regularexpression.cpp
INCLUDE_DIRECTORIES
../../../other/qabstractitemmodelutils
LIBRARIES
Qt::TestPrivate
)

View File

@ -0,0 +1,130 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QSignalSpy>
#include <QStringListModel>
#include <QSortFilterProxyModel>
class tst_QSortFilterProxyModelRegularExpression : public QObject
{
Q_OBJECT
private slots:
void tst_invalid();
void tst_caseSensitivity();
void tst_keepCaseSensitivity_QTBUG_92260();
void tst_keepPatternOptions_QTBUG_92260();
void tst_regexCaseSensitivityNotification();
};
void tst_QSortFilterProxyModelRegularExpression::tst_invalid()
{
const QLatin1String pattern("test");
QSortFilterProxyModel model;
model.setFilterRegularExpression(pattern);
QCOMPARE(model.filterRegularExpression(), QRegularExpression(pattern));
}
void tst_QSortFilterProxyModelRegularExpression::tst_caseSensitivity()
{
const QLatin1String pattern("test");
QStringListModel model({ "test", "TesT" });
QSortFilterProxyModel proxyModel;
proxyModel.setSourceModel(&model);
proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel.setFilterRegularExpression(pattern);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseInsensitive);
QCOMPARE(proxyModel.rowCount(), 2);
proxyModel.setFilterCaseSensitivity(Qt::CaseSensitive);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseSensitive);
QCOMPARE(proxyModel.rowCount(), 1);
proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseInsensitive);
QCOMPARE(proxyModel.rowCount(), 2);
}
/*!
This test ensures that when a string pattern is passed to setRegularEpxression,
the options are properly reseted but that the case sensitivity is kept as is.
*/
void tst_QSortFilterProxyModelRegularExpression::tst_keepCaseSensitivity_QTBUG_92260()
{
const QLatin1String pattern("test");
QStringListModel model({ "test", "TesT" });
QSortFilterProxyModel proxyModel;
proxyModel.setSourceModel(&model);
QRegularExpression patternWithOptions("Dummy",
QRegularExpression::MultilineOption
| QRegularExpression::CaseInsensitiveOption);
proxyModel.setFilterRegularExpression(patternWithOptions);
QCOMPARE(proxyModel.filterRegularExpression(), patternWithOptions);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseInsensitive);
proxyModel.setFilterRegularExpression(pattern);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseInsensitive);
QCOMPARE(proxyModel.filterRegularExpression().patternOptions(),
QRegularExpression::CaseInsensitiveOption);
patternWithOptions.setPatternOptions(QRegularExpression::MultilineOption);
proxyModel.setFilterRegularExpression(patternWithOptions);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseSensitive);
QCOMPARE(proxyModel.filterRegularExpression(), patternWithOptions);
proxyModel.setFilterRegularExpression(pattern);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseSensitive);
QCOMPARE(proxyModel.filterRegularExpression().patternOptions(),
QRegularExpression::NoPatternOption);
}
/*!
This test ensures that when the case sensitivity is changed, it does not nuke
the pattern options that were set before.
*/
void tst_QSortFilterProxyModelRegularExpression::tst_keepPatternOptions_QTBUG_92260()
{
QStringListModel model({ "test", "TesT" });
QSortFilterProxyModel proxyModel;
proxyModel.setSourceModel(&model);
QRegularExpression patternWithOptions("Dummy",
QRegularExpression::MultilineOption
| QRegularExpression::CaseInsensitiveOption);
proxyModel.setFilterRegularExpression(patternWithOptions);
QCOMPARE(proxyModel.filterRegularExpression(), patternWithOptions);
proxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseInsensitive);
QCOMPARE(proxyModel.filterRegularExpression().patternOptions(),
patternWithOptions.patternOptions());
proxyModel.setFilterCaseSensitivity(Qt::CaseSensitive);
QCOMPARE(proxyModel.filterCaseSensitivity(), Qt::CaseSensitive);
QCOMPARE(proxyModel.filterRegularExpression().patternOptions(),
QRegularExpression::MultilineOption);
}
/*!
This test ensures that if the case sensitivity is changed during a call to
setFilterRegularExpression, the notification signal will be emitted
*/
void tst_QSortFilterProxyModelRegularExpression::tst_regexCaseSensitivityNotification()
{
QSortFilterProxyModel proxy;
QSignalSpy spy(&proxy, &QSortFilterProxyModel::filterCaseSensitivityChanged);
proxy.setFilterCaseSensitivity(Qt::CaseInsensitive);
QCOMPARE(spy.size(), 1);
QRegularExpression re("regex");
QVERIFY(!re.patternOptions().testFlag(QRegularExpression::CaseInsensitiveOption));
proxy.setFilterRegularExpression(re);
QCOMPARE(proxy.filterCaseSensitivity(), Qt::CaseSensitive);
QCOMPARE(spy.size(), 2);
}
QTEST_MAIN(tst_QSortFilterProxyModelRegularExpression)
#include "tst_qsortfilterproxymodel_regularexpression.moc"

View File

@ -0,0 +1,12 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qstringlistmodel Test:
#####################################################################
qt_internal_add_test(tst_qstringlistmodel
SOURCES
qmodellistener.h
tst_qstringlistmodel.cpp
)

View File

@ -0,0 +1,36 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QObject>
#include <QModelIndex>
#include <qdebug.h>
QT_FORWARD_DECLARE_CLASS(QStringListModel)
class QModelListener : public QObject
{
Q_OBJECT
public:
QModelListener(QStringList *pAboutToStringlist, QStringList *pExpectedStringlist, QStringListModel *pModel)
{
setTestData(pAboutToStringlist, pExpectedStringlist, pModel);
}
virtual ~QModelListener() { }
void setTestData(QStringList *pAboutToStringlist, QStringList *pExpectedStringlist, QStringListModel *pModel)
{
m_pAboutToStringlist = pAboutToStringlist;
m_pExpectedStringlist = pExpectedStringlist;
m_pModel = pModel;
}
private:
QStringList *m_pAboutToStringlist;
QStringList *m_pExpectedStringlist;
QStringListModel *m_pModel;
public slots:
void rowsAboutToBeRemovedOrInserted(const QModelIndex & parent, int start, int end );
void rowsRemovedOrInserted(const QModelIndex & parent, int start, int end );
};

View File

@ -0,0 +1,456 @@
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QTest>
#include <QSignalSpy>
#include <qabstractitemmodel.h>
#include <qcoreapplication.h>
#include <qmap.h>
#include <qstringlistmodel.h>
#include <qstringlist.h>
#include "qmodellistener.h"
#include <qstringlistmodel.h>
#include <algorithm>
void QModelListener::rowsAboutToBeRemovedOrInserted(const QModelIndex & parent, int start, int end )
{
for (int i = 0; start + i <= end; i++) {
QModelIndex mIndex = m_pModel->index(start + i, 0, parent);
QVariant var = m_pModel->data(mIndex, Qt::DisplayRole);
QString str = var.toString();
QCOMPARE(str, m_pAboutToStringlist->at(i));
}
}
void QModelListener::rowsRemovedOrInserted(const QModelIndex & parent, int , int)
{
// Can the rows that *are* removed be iterated now ?
// What about rowsAboutToBeInserted - what will the indices be?
// will insertRow() overwrite existing, or insert (and conseq. grow the model?)
// What will the item then contain? empty data?
// RemoveColumn. Does that also fire the rowsRemoved-family signals?
for (int i = 0; i < m_pExpectedStringlist->size(); i++) {
QModelIndex mIndex = m_pModel->index(i, 0, parent);
QVariant var = m_pModel->data(mIndex, Qt::DisplayRole);
QString str = var.toString();
QCOMPARE(str, m_pExpectedStringlist->at(i));
}
}
class tst_QStringListModel : public QObject
{
Q_OBJECT
private slots:
void rowsAboutToBeRemoved_rowsRemoved();
void rowsAboutToBeRemoved_rowsRemoved_data();
void rowsAboutToBeInserted_rowsInserted();
void rowsAboutToBeInserted_rowsInserted_data();
void setData_emits_both_roles_data();
void setData_emits_both_roles();
void setData_emits_on_change_only();
void supportedDragDropActions();
void moveRows_data();
void moveRows();
void moveRowsInvalid_data();
void moveRowsInvalid();
void itemData();
void setItemData();
void createPersistentOnLayoutAboutToBeChanged();
};
void tst_QStringListModel::moveRowsInvalid_data()
{
QTest::addColumn<QStringListModel*>("baseModel");
QTest::addColumn<QModelIndex>("startParent");
QTest::addColumn<int>("startRow");
QTest::addColumn<int>("count");
QTest::addColumn<QModelIndex>("destinationParent");
QTest::addColumn<int>("destination");
const auto createModel = [this]() {
return new QStringListModel(QStringList{"A", "B", "C", "D", "E", "F"}, this);
};
constexpr int rowCount = 6;
QTest::addRow("destination_equal_source") << createModel() << QModelIndex() << 0 << 1 << QModelIndex() << 0;
QTest::addRow("count_equal_0") << createModel() << QModelIndex() << 0 << 0 << QModelIndex() << 2;
QStringListModel *tempModel = createModel();
QTest::addRow("move_child") << tempModel << tempModel->index(0, 0) << 0 << 1 << QModelIndex() << 2;
tempModel = createModel();
QTest::addRow("move_to_child") << tempModel << QModelIndex() << 0 << 1 << tempModel->index(0, 0) << 2;
QTest::addRow("negative_count") << createModel() << QModelIndex() << 0 << -1 << QModelIndex() << 2;
QTest::addRow("negative_source_row") << createModel() << QModelIndex() << -1 << 1 << QModelIndex() << 2;
QTest::addRow("negative_destination_row") << createModel() << QModelIndex() << 0 << 1 << QModelIndex() << -1;
QTest::addRow("source_row_equal_rowCount") << createModel() << QModelIndex() << rowCount << 1 << QModelIndex() << 1;
QTest::addRow("source_row_equal_destination_row") << createModel() << QModelIndex() << 2 << 1 << QModelIndex() << 2;
QTest::addRow("source_row_equal_destination_row_plus_1") << createModel() << QModelIndex() << 2 << 1 << QModelIndex() << 3;
QTest::addRow("destination_row_greater_rowCount") << createModel() << QModelIndex() << 0 << 1 << QModelIndex() << rowCount + 1;
QTest::addRow("move_row_within_source_range") << createModel() << QModelIndex() << 0 << 3 << QModelIndex() << 2;
}
void tst_QStringListModel::moveRowsInvalid()
{
QFETCH(QStringListModel* const, baseModel);
QFETCH(const QModelIndex, startParent);
QFETCH(const int, startRow);
QFETCH(const int, count);
QFETCH(const QModelIndex, destinationParent);
QFETCH(const int, destination);
QSignalSpy rowMovedSpy(baseModel, &QAbstractItemModel::rowsMoved);
QSignalSpy rowAboutMovedSpy(baseModel, &QAbstractItemModel::rowsAboutToBeMoved);
QVERIFY(rowMovedSpy.isValid());
QVERIFY(rowAboutMovedSpy.isValid());
QVERIFY(!baseModel->moveRows(startParent, startRow, count, destinationParent, destination));
QCOMPARE(rowMovedSpy.size(), 0);
QCOMPARE(rowAboutMovedSpy.size(), 0);
delete baseModel;
}
void tst_QStringListModel::moveRows_data()
{
QTest::addColumn<int>("startRow");
QTest::addColumn<int>("count");
QTest::addColumn<int>("destination");
QTest::addColumn<QStringList>("expected");
QTest::newRow("1_Item_from_top_to_middle") << 0 << 1 << 3 << QStringList{"B", "C", "A", "D", "E", "F"};
QTest::newRow("1_Item_from_top_to_bottom") << 0 << 1 << 6 << QStringList{"B", "C", "D", "E", "F", "A"};
QTest::newRow("1_Item_from_middle_to_top") << 2 << 1 << 0 << QStringList{"C", "A", "B", "D", "E", "F"};
QTest::newRow("1_Item_from_bottom_to_middle") << 5 << 1 << 2 << QStringList{"A", "B", "F", "C", "D", "E"};
QTest::newRow("1_Item_from_bottom to_top") << 5 << 1 << 0 << QStringList{"F", "A", "B", "C", "D", "E"};
QTest::newRow("1_Item_from_middle_to_bottom") << 2 << 1 << 6 << QStringList{"A", "B", "D", "E", "F", "C"};
QTest::newRow("1_Item_from_middle_to_middle_before") << 2 << 1 << 1 << QStringList{"A", "C", "B", "D", "E", "F"};
QTest::newRow("1_Item_from_middle_to_middle_after") << 2 << 1 << 4 << QStringList{"A", "B", "D", "C", "E", "F"};
QTest::newRow("2_Items_from_top_to_middle") << 0 << 2 << 3 << QStringList{"C", "A", "B", "D", "E", "F"};
QTest::newRow("2_Items_from_top_to_bottom") << 0 << 2 << 6 << QStringList{"C", "D", "E", "F", "A", "B"};
QTest::newRow("2_Items_from_middle_to_top") << 2 << 2 << 0 << QStringList{"C", "D", "A", "B", "E", "F"};
QTest::newRow("2_Items_from_bottom_to_middle") << 4 << 2 << 2 << QStringList{"A", "B", "E", "F", "C", "D"};
QTest::newRow("2_Items_from_bottom_to_top") << 4 << 2 << 0 << QStringList{"E", "F", "A", "B", "C", "D"};
QTest::newRow("2_Items_from_middle_to_bottom") << 2 << 2 << 6 << QStringList{"A", "B", "E", "F", "C", "D"};
QTest::newRow("2_Items_from_middle_to_middle_before") << 3 << 2 << 1 << QStringList{"A", "D", "E", "B", "C", "F"};
QTest::newRow("2_Items_from_middle_to_middle_after") << 1 << 2 << 5 << QStringList{"A", "D", "E", "B", "C", "F"};
}
void tst_QStringListModel::moveRows()
{
QFETCH(const int, startRow);
QFETCH(const int, count);
QFETCH(const int, destination);
QFETCH(const QStringList, expected);
QStringListModel baseModel(QStringList{"A", "B", "C", "D", "E", "F"});
QSignalSpy rowMovedSpy(&baseModel, &QAbstractItemModel::rowsMoved);
QSignalSpy rowAboutMovedSpy(&baseModel, &QAbstractItemModel::rowsAboutToBeMoved);
QVERIFY(baseModel.moveRows(QModelIndex(), startRow, count, QModelIndex(), destination));
QCOMPARE(baseModel.stringList(), expected);
QCOMPARE(rowMovedSpy.size(), 1);
QCOMPARE(rowAboutMovedSpy.size(), 1);
for (const QList<QVariant> &signalArgs : {rowMovedSpy.first(), rowAboutMovedSpy.first()}){
QVERIFY(!signalArgs.at(0).value<QModelIndex>().isValid());
QCOMPARE(signalArgs.at(1).toInt(), startRow);
QCOMPARE(signalArgs.at(2).toInt(), startRow + count - 1);
QVERIFY(!signalArgs.at(3).value<QModelIndex>().isValid());
QCOMPARE(signalArgs.at(4).toInt(), destination);
}
}
void tst_QStringListModel::rowsAboutToBeRemoved_rowsRemoved_data()
{
QTest::addColumn<QStringList>("input");
QTest::addColumn<int>("row");
QTest::addColumn<int>("count");
QTest::addColumn<QStringList>("aboutto");
QTest::addColumn<QStringList>("res");
QStringList strings0; strings0 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto0; aboutto0 << "Two" << "Three";
QStringList res0; res0 << "One" << "Four" << "Five";
QTest::newRow( "data0" ) << strings0 << 1 << 2 << aboutto0 << res0;
QStringList strings1; strings1 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto1; aboutto1 << "One" << "Two";
QStringList res1; res1 << "Three" << "Four" << "Five";
QTest::newRow( "data1" ) << strings1 << 0 << 2 << aboutto1 << res1;
QStringList strings2; strings2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto2; aboutto2 << "Four" << "Five";
QStringList res2; res2 << "One" << "Two" << "Three";
QTest::newRow( "data2" ) << strings2 << 3 << 2 << aboutto2 << res2;
QStringList strings3; strings3 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto3; aboutto3 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList res3;
QTest::newRow( "data3" ) << strings3 << 0 << 5 << aboutto3 << res3;
/*
* Keep this, template to add more data
QStringList strings2; strings2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto2; aboutto2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList res2; res2 << "One" << "Two" << "Three" << "Four" << "Five";
QTest::newRow( "data2" ) << strings2 << 0 << 5 << aboutto2 << res2;
*/
}
void tst_QStringListModel::rowsAboutToBeRemoved_rowsRemoved()
{
QFETCH(QStringList, input);
QFETCH(int, row);
QFETCH(int, count);
QFETCH(QStringList, aboutto);
QFETCH(QStringList, res);
QStringListModel model(input);
QModelListener listener(&aboutto, &res, &model);
connect(&model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
&listener, SLOT(rowsAboutToBeRemovedOrInserted(QModelIndex,int,int)));
connect(&model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
&listener, SLOT(rowsRemovedOrInserted(QModelIndex,int,int)));
model.removeRows(row, count);
// At this point, control goes to our connected slots inn this order:
// 1. rowsAboutToBeRemovedOrInserted
// 2. rowsRemovedOrInserted
// Control returns here
}
void tst_QStringListModel::rowsAboutToBeInserted_rowsInserted_data()
{
QTest::addColumn<QStringList>("input");
QTest::addColumn<int>("row");
QTest::addColumn<int>("count");
QTest::addColumn<QStringList>("aboutto");
QTest::addColumn<QStringList>("res");
QStringList strings0; strings0 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto0; aboutto0 << "Two" << "Three";
QStringList res0; res0 << "One" << "" << "" << "Two" << "Three" << "Four" << "Five";
QTest::newRow( "data0" ) << strings0 << 1 << 2 << aboutto0 << res0;
QStringList strings1; strings1 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto1; aboutto1 << "One" << "Two";
QStringList res1; res1 << "" << "" << "One" << "Two" << "Three" << "Four" << "Five";
QTest::newRow( "data1" ) << strings1 << 0 << 2 << aboutto1 << res1;
QStringList strings2; strings2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto2; aboutto2 << "Four" << "Five";
QStringList res2; res2 << "One" << "Two" << "Three" << "" << "" << "Four" << "Five";
QTest::newRow( "data2" ) << strings2 << 3 << 2 << aboutto2 << res2;
QStringList strings3; strings3 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto3; aboutto3 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList res3; res3 << "" << "" << "" << "" << "" << "One" << "Two" << "Three" << "Four" << "Five";
QTest::newRow( "data3" ) << strings3 << 0 << 5 << aboutto3 << res3;
/*
* Keep this, template to add more data
QStringList strings2; strings2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList aboutto2; aboutto2 << "One" << "Two" << "Three" << "Four" << "Five";
QStringList res2; res2 << "One" << "Two" << "Three" << "Four" << "Five";
QTest::newRow( "data2" ) << strings2 << 0 << 5 << aboutto2 << res2;
*/
}
void tst_QStringListModel::rowsAboutToBeInserted_rowsInserted()
{
QFETCH(QStringList, input);
QFETCH(int, row);
QFETCH(int, count);
QFETCH(QStringList, aboutto);
QFETCH(QStringList, res);
QStringListModel model(input);
QModelListener listener(&aboutto, &res, &model);
connect(&model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
&listener, SLOT(rowsAboutToBeRemovedOrInserted(QModelIndex,int,int)));
connect(&model, SIGNAL(rowsInserted(QModelIndex,int,int)),
&listener, SLOT(rowsRemovedOrInserted(QModelIndex,int,int)));
model.insertRows(row, count);
// At this point, control goes to our connected slots inn this order:
// 1. rowsAboutToBeRemovedOrInserted
// 2. rowsRemovedOrInserted
// Control returns here
}
void tst_QStringListModel::setData_emits_both_roles_data()
{
QTest::addColumn<int>("row");
QTest::addColumn<QString>("data");
QTest::addColumn<int>("role");
#define ROW(row, string, role) \
QTest::newRow(#row " -> " string) << row << QString(string) << int(Qt::role)
ROW(0, "1", EditRole);
ROW(1, "2", DisplayRole);
#undef ROW
}
template <class C>
C sorted(C c)
{
std::sort(c.begin(), c.end());
return c;
}
void tst_QStringListModel::setData_emits_both_roles()
{
QFETCH(int, row);
QFETCH(QString, data);
QFETCH(int, role);
QStringListModel model(QStringList() << "one" << "two");
QList<int> expected;
expected.reserve(2);
expected.append(Qt::DisplayRole);
expected.append(Qt::EditRole);
QSignalSpy spy(&model, &QAbstractItemModel::dataChanged);
QVERIFY(spy.isValid());
model.setData(model.index(row, 0), data, role);
QCOMPARE(spy.size(), 1);
QCOMPARE(sorted(spy.at(0).at(2).value<QList<int> >()),
expected);
}
void tst_QStringListModel::itemData()
{
QStringListModel testModel{ QStringList {
QStringLiteral("One"),
QStringLiteral("Two"),
QStringLiteral("Three"),
QStringLiteral("Four"),
QStringLiteral("Five")
}};
QMap<int, QVariant> compareMap;
QCOMPARE(testModel.itemData(QModelIndex()), compareMap);
compareMap.insert(Qt::DisplayRole, QStringLiteral("Two"));
compareMap.insert(Qt::EditRole, QStringLiteral("Two"));
QCOMPARE(testModel.itemData(testModel.index(1, 0)), compareMap);
}
void tst_QStringListModel::setItemData()
{
QStringListModel testModel{ QStringList {
QStringLiteral("One"),
QStringLiteral("Two"),
QStringLiteral("Three"),
QStringLiteral("Four"),
QStringLiteral("Five")
}};
QSignalSpy dataChangedSpy(&testModel, &QAbstractItemModel::dataChanged);
QModelIndex changeIndex = testModel.index(1, 0);
const QList<int> changeRoles{Qt::DisplayRole, Qt::EditRole};
const QString changedString("Changed");
QMap<int, QVariant> newItemData{std::make_pair<int>(Qt::DisplayRole, changedString)};
// invalid index does nothing and returns false
QVERIFY(!testModel.setItemData(QModelIndex(), newItemData));
// valid data is set, return value is true and dataChanged is emitted once
QVERIFY(testModel.setItemData(changeIndex, newItemData));
QCOMPARE(changeIndex.data(Qt::DisplayRole).toString(), changedString);
QCOMPARE(changeIndex.data(Qt::EditRole).toString(), changedString);
QCOMPARE(dataChangedSpy.size(), 1);
QVariantList dataChangedArguments = dataChangedSpy.takeFirst();
QCOMPARE(dataChangedArguments.at(0).value<QModelIndex>(), changeIndex);
QCOMPARE(dataChangedArguments.at(1).value<QModelIndex>(), changeIndex);
QCOMPARE(dataChangedArguments.at(2).value<QList<int> >(), changeRoles);
// Unsupported roles do nothing return false
newItemData.clear();
newItemData.insert(Qt::UserRole, changedString);
QVERIFY(!testModel.setItemData(changeIndex, newItemData));
QCOMPARE(dataChangedSpy.size(), 0);
// If some but not all the roles are supported it returns false and does nothing
newItemData.insert(Qt::EditRole, changedString);
changeIndex = testModel.index(2, 0);
QVERIFY(!testModel.setItemData(changeIndex, newItemData));
QCOMPARE(changeIndex.data(Qt::DisplayRole).toString(), QStringLiteral("Three"));
QCOMPARE(changeIndex.data(Qt::EditRole).toString(), QStringLiteral("Three"));
QCOMPARE(dataChangedSpy.size(), 0);
// Qt::EditRole and Qt::DisplayRole are both set, Qt::EditRole takes precedence
newItemData.clear();
newItemData.insert(Qt::EditRole, changedString);
newItemData.insert(Qt::DisplayRole, QStringLiteral("Ignored"));
changeIndex = testModel.index(3, 0);
QVERIFY(testModel.setItemData(changeIndex, newItemData));
QCOMPARE(changeIndex.data(Qt::DisplayRole).toString(), changedString);
QCOMPARE(changeIndex.data(Qt::EditRole).toString(), changedString);
QCOMPARE(dataChangedSpy.size(), 1);
dataChangedArguments = dataChangedSpy.takeFirst();
QCOMPARE(dataChangedArguments.at(0).value<QModelIndex>(), changeIndex);
QCOMPARE(dataChangedArguments.at(1).value<QModelIndex>(), changeIndex);
QCOMPARE(dataChangedArguments.at(2).value<QList<int> >(), changeRoles);
}
void tst_QStringListModel::setData_emits_on_change_only()
{
QStringListModel model(QStringList{QStringLiteral("one"), QStringLiteral("two")});
QSignalSpy dataChangedSpy(&model, &QAbstractItemModel::dataChanged);
QVERIFY(dataChangedSpy.isValid());
const QModelIndex modelIdx = model.index(0, 0);
const QString newStringData = QStringLiteral("test");
QVERIFY(model.setData(modelIdx, newStringData));
QCOMPARE(dataChangedSpy.size(), 1);
const QList<QVariant> spyList = dataChangedSpy.takeFirst();
QCOMPARE(spyList.at(0).value<QModelIndex>(), modelIdx);
QCOMPARE(spyList.at(1).value<QModelIndex>(), modelIdx);
const QList<int> expectedRoles{Qt::DisplayRole, Qt::EditRole};
QCOMPARE(spyList.at(2).value<QList<int> >(), expectedRoles);
QVERIFY(model.setData(modelIdx, newStringData));
QVERIFY(dataChangedSpy.isEmpty());
}
void tst_QStringListModel::supportedDragDropActions()
{
QStringListModel model;
QCOMPARE(model.supportedDragActions(), Qt::CopyAction | Qt::MoveAction);
QCOMPARE(model.supportedDropActions(), Qt::CopyAction | Qt::MoveAction);
}
void tst_QStringListModel::createPersistentOnLayoutAboutToBeChanged() // QTBUG-93466
{
QStringListModel model(QStringList{QStringLiteral("1"), QStringLiteral("2"), QStringLiteral("3")});
QList<QPersistentModelIndex> idxList;
QSignalSpy layoutAboutToBeChangedSpy(&model, &QAbstractItemModel::layoutAboutToBeChanged);
QSignalSpy layoutChangedSpy(&model, &QAbstractItemModel::layoutChanged);
connect(&model, &QAbstractItemModel::layoutAboutToBeChanged, this, [&idxList, &model](){
idxList.clear();
for (int row = 0; row < 3; ++row)
idxList << QPersistentModelIndex(model.index(row, 0));
});
connect(&model, &QAbstractItemModel::layoutChanged, this, [&idxList](){
QCOMPARE(idxList.size(), 3);
QCOMPARE(idxList.at(0).row(), 1);
QCOMPARE(idxList.at(0).column(), 0);
QCOMPARE(idxList.at(0).data().toString(), QStringLiteral("1"));
QCOMPARE(idxList.at(1).row(), 0);
QCOMPARE(idxList.at(1).column(), 0);
QCOMPARE(idxList.at(1).data().toString(), QStringLiteral("0"));
QCOMPARE(idxList.at(2).row(), 2);
QCOMPARE(idxList.at(2).column(), 0);
QCOMPARE(idxList.at(2).data().toString(), QStringLiteral("3"));
});
model.setData(model.index(1, 0), QStringLiteral("0"));
model.sort(0);
QCOMPARE(layoutAboutToBeChangedSpy.size(), 1);
QCOMPARE(layoutChangedSpy.size(), 1);
}
QTEST_MAIN(tst_QStringListModel)
#include "tst_qstringlistmodel.moc"

View File

@ -0,0 +1,13 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
#####################################################################
## tst_qtransposeproxymodel Test:
#####################################################################
qt_internal_add_test(tst_qtransposeproxymodel
SOURCES
tst_qtransposeproxymodel.cpp
LIBRARIES
Qt::Gui
)

View File

@ -0,0 +1,962 @@
// Copyright (C) 2018 Luca Beldi <v.ronin@yahoo.it>
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QTest>
#include <QSignalSpy>
#include <QStandardItemModel>
#include <QStringListModel>
#include <QAbstractItemModelTester>
#include <random>
#include <qtransposeproxymodel.h>
class tst_QTransposeProxyModel : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void index();
void data();
void setData_data();
void setData();
void parent();
void mapToSource();
void mapFromSource();
void basicTest_data();
void basicTest();
void sort();
void insertRowBase_data();
void insertRowBase();
void insertColumnBase_data();
void insertColumnBase();
void insertColumnProxy_data();
void insertColumnProxy();
void insertRowProxy_data();
void insertRowProxy();
void removeRowBase_data();
void removeRowBase();
void removeColumnBase_data();
void removeColumnBase();
void removeColumnProxy_data();
void removeColumnProxy();
void removeRowProxy_data();
void removeRowProxy();
void headerData();
void setHeaderData();
void span();
void itemData();
void setItemData();
void moveRowsBase();
void moveColumnsProxy();
void sortPersistentIndex();
void createPersistentOnLayoutAboutToBeChanged();
private:
void testTransposed(
const QAbstractItemModel *const baseModel,
const QAbstractItemModel *const transposed,
const QModelIndex &baseParent = QModelIndex(),
const QModelIndex &transposedParent = QModelIndex()
);
QAbstractItemModel *createListModel(QObject *parent);
QAbstractItemModel *createTableModel(QObject *parent);
QAbstractItemModel *createTreeModel(QObject *parent);
};
QAbstractItemModel *tst_QTransposeProxyModel::createListModel(QObject *parent)
{
QStringList sequence;
sequence.reserve(10);
for (int i = 0; i < 10; ++i)
sequence.append(QString::number(i));
return new QStringListModel(sequence, parent);
}
QAbstractItemModel *tst_QTransposeProxyModel::createTableModel(QObject *parent)
{
QAbstractItemModel *model = new QStandardItemModel(parent);
model->insertRows(0, 5);
model->insertColumns(0, 4);
for (int i = 0; i < model->rowCount(); ++i) {
for (int j = 0; j < model->columnCount(); ++j) {
model->setData(model->index(i, j), QStringLiteral("%1,%2").arg(i).arg(j), Qt::EditRole);
model->setData(model->index(i, j), i, Qt::UserRole);
model->setData(model->index(i, j), j, Qt::UserRole + 1);
}
}
return model;
}
QAbstractItemModel *tst_QTransposeProxyModel::createTreeModel(QObject *parent)
{
QAbstractItemModel *model = new QStandardItemModel(parent);
model->insertRows(0, 5);
model->insertColumns(0, 4);
for (int i = 0; i < model->rowCount(); ++i) {
for (int j = 0; j < model->columnCount(); ++j) {
const QModelIndex parIdx = model->index(i, j);
model->setData(parIdx, QStringLiteral("%1,%2").arg(i).arg(j), Qt::EditRole);
model->setData(parIdx, i, Qt::UserRole);
model->setData(parIdx, j, Qt::UserRole + 1);
model->insertRows(0, 3, parIdx);
model->insertColumns(0, 2, parIdx);
for (int h = 0; h < model->rowCount(parIdx); ++h) {
for (int k = 0; k < model->columnCount(parIdx); ++k) {
const QModelIndex childIdx = model->index(h, k, parIdx);
model->setData(childIdx, QStringLiteral("%1,%2,%3,%4").arg(i).arg(j).arg(h).arg(k), Qt::EditRole);
model->setData(childIdx, i, Qt::UserRole);
model->setData(childIdx, j, Qt::UserRole + 1);
model->setData(childIdx, h, Qt::UserRole + 2);
model->setData(childIdx, k, Qt::UserRole + 3);
}
}
}
}
return model;
}
void tst_QTransposeProxyModel::testTransposed(
const QAbstractItemModel *const baseModel,
const QAbstractItemModel *const transposed,
const QModelIndex &baseParent,
const QModelIndex &transposedParent
)
{
QCOMPARE(transposed->hasChildren(transposedParent), baseModel->hasChildren(baseParent));
QCOMPARE(transposed->columnCount(transposedParent), baseModel->rowCount(baseParent));
QCOMPARE(transposed->rowCount(transposedParent), baseModel->columnCount(baseParent));
for (int i = 0, maxRow = baseModel->rowCount(baseParent); i < maxRow; ++i) {
for (int j = 0, maxCol = baseModel->columnCount(baseParent); j < maxCol; ++j) {
const QModelIndex baseIdx = baseModel->index(i, j, baseParent);
const QModelIndex transIdx = transposed->index(j, i, transposedParent);
QCOMPARE(transIdx.data(), baseIdx.data());
QCOMPARE(transIdx.data(Qt::UserRole), baseIdx.data(Qt::UserRole));
QCOMPARE(transIdx.data(Qt::UserRole + 1), baseIdx.data(Qt::UserRole + 1));
QCOMPARE(transIdx.data(Qt::UserRole + 2), baseIdx.data(Qt::UserRole + 2));
QCOMPARE(transIdx.data(Qt::UserRole + 3), baseIdx.data(Qt::UserRole + 3));
if (baseModel->hasChildren(baseIdx)) {
testTransposed(baseModel, transposed, baseIdx, transIdx);
}
}
}
}
void tst_QTransposeProxyModel::initTestCase()
{
qRegisterMetaType<QList<QPersistentModelIndex> >();
qRegisterMetaType<QAbstractItemModel::LayoutChangeHint>();
}
void tst_QTransposeProxyModel::index()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QVERIFY(!proxy.index(0, -1).isValid());
QVERIFY(!proxy.index(0, -1).isValid());
QVERIFY(!proxy.index(-1, -1).isValid());
QVERIFY(!proxy.index(0, proxy.columnCount()).isValid());
QVERIFY(!proxy.index(proxy.rowCount(), 0).isValid());
QVERIFY(!proxy.index(proxy.rowCount(), proxy.columnCount()).isValid());
QModelIndex tempIdx = proxy.index(0, 1);
QVERIFY(tempIdx.isValid());
QCOMPARE(tempIdx.row(), 0);
QCOMPARE(tempIdx.column(), 1);
tempIdx = proxy.index(0, 1, tempIdx);
QVERIFY(tempIdx.isValid());
QCOMPARE(tempIdx.row(), 0);
QCOMPARE(tempIdx.column(), 1);
delete model;
}
void tst_QTransposeProxyModel::data()
{
QStringListModel model{QStringList{"A", "B"}};
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QCOMPARE(proxy.index(0, 1).data().toString(), QStringLiteral("B"));
}
void tst_QTransposeProxyModel::parent()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
const QModelIndex parentIdx = proxy.index(0, 0);
const QModelIndex childIdx = proxy.index(0, 0, parentIdx);
QVERIFY(parentIdx.isValid());
QVERIFY(childIdx.isValid());
QCOMPARE(childIdx.parent(), parentIdx);
delete model;
}
void tst_QTransposeProxyModel::mapToSource()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QVERIFY(!proxy.mapToSource(QModelIndex()).isValid());
QCOMPARE(proxy.mapToSource(proxy.index(0, 0)), model->index(0, 0));
QCOMPARE(proxy.mapToSource(proxy.index(1, 0)), model->index(0, 1));
QCOMPARE(proxy.mapToSource(proxy.index(0, 1)), model->index(1, 0));
const QModelIndex proxyParent = proxy.index(1, 0);
const QModelIndex sourceParent = model->index(0, 1);
QCOMPARE(proxy.mapToSource(proxy.index(0, 0, proxyParent)), model->index(0, 0, sourceParent));
QCOMPARE(proxy.mapToSource(proxy.index(1, 0, proxyParent)), model->index(0, 1, sourceParent));
QCOMPARE(proxy.mapToSource(proxy.index(0, 1, proxyParent)), model->index(1, 0, sourceParent));
delete model;
}
void tst_QTransposeProxyModel::mapFromSource()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QVERIFY(!proxy.mapFromSource(QModelIndex()).isValid());
QCOMPARE(proxy.mapFromSource(model->index(0, 0)), proxy.index(0, 0));
QCOMPARE(proxy.mapFromSource(model->index(0, 1)), proxy.index(1, 0));
QCOMPARE(proxy.mapFromSource(model->index(1, 0)), proxy.index(0, 1));
const QModelIndex proxyParent = proxy.index(1, 0);
const QModelIndex sourceParent = model->index(0, 1);
QCOMPARE(proxy.mapToSource(proxy.index(0, 0, proxyParent)), model->index(0, 0, sourceParent));
QCOMPARE(proxy.mapFromSource(model->index(1, 0, sourceParent)), proxy.index(0, 1, proxyParent));
QCOMPARE(proxy.mapFromSource(model->index(0, 1, sourceParent)), proxy.index(1, 0, proxyParent));
delete model;
}
void tst_QTransposeProxyModel::basicTest_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::newRow("List") << createListModel(this);
QTest::newRow("Table") << createTableModel(this);
QTest::newRow("Tree") << createTreeModel(this);
}
void tst_QTransposeProxyModel::basicTest()
{
QFETCH(QAbstractItemModel *, model);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
testTransposed(model, &proxy);
delete model;
}
void tst_QTransposeProxyModel::sort()
{
QStringList sequence;
sequence.reserve(100);
for (int i = 0; i < 100; ++i)
sequence.append(QStringLiteral("%1").arg(i, 3, 10, QLatin1Char('0')));
std::shuffle(sequence.begin(), sequence.end(), std::mt19937(88));
const QString firstItemBeforeSort = sequence.first();
QStringListModel baseModel(sequence);
QTransposeProxyModel proxyModel;
new QAbstractItemModelTester(&proxyModel, &proxyModel);
proxyModel.setSourceModel(&baseModel);
QSignalSpy layoutChangedSpy(&proxyModel, &QAbstractItemModel::layoutChanged);
QVERIFY(layoutChangedSpy.isValid());
QSignalSpy layoutAboutToBeChangedSpy(&proxyModel, &QAbstractItemModel::layoutAboutToBeChanged);
QVERIFY(layoutAboutToBeChangedSpy.isValid());
QPersistentModelIndex firstIndexBeforeSort = proxyModel.index(0, 0);
baseModel.sort(0, Qt::AscendingOrder);
QCOMPARE(layoutChangedSpy.size(), 1);
QCOMPARE(layoutAboutToBeChangedSpy.size(), 1);
QCOMPARE(layoutChangedSpy.takeFirst().at(1).toInt(), int(QAbstractItemModel::HorizontalSortHint));
QCOMPARE(firstIndexBeforeSort.data().toString(), firstItemBeforeSort);
for (int i = 0; i < 100; ++i)
QCOMPARE(proxyModel.index(0, i).data().toInt(), i);
}
void tst_QTransposeProxyModel::removeColumnBase_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<QModelIndex>("parent");
QTest::newRow("Table") << createTableModel(this) << QModelIndex();
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << QModelIndex();
QAbstractItemModel *model = createTreeModel(this);
QTest::newRow("Tree_Child_Item") << model << model->index(0, 0);
}
void tst_QTransposeProxyModel::removeColumnBase()
{
QFETCH(QAbstractItemModel * const, model);
QFETCH(const QModelIndex, parent);
QTransposeProxyModel proxy;
QSignalSpy rowRemoveSpy(&proxy, &QAbstractItemModel::rowsRemoved);
QVERIFY(rowRemoveSpy.isValid());
QSignalSpy rowAboutToBeRemoveSpy(&proxy, &QAbstractItemModel::rowsAboutToBeRemoved);
QVERIFY(rowAboutToBeRemoveSpy.isValid());
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
const int oldRowCount = proxy.rowCount(proxy.mapFromSource(parent));
const QVariant expectedNewVal = model->index(0, 2, parent).data();
QVERIFY(model->removeColumn(1, parent));
QCOMPARE(proxy.rowCount(proxy.mapFromSource(parent)), oldRowCount - 1);
QCOMPARE(proxy.index(1, 0, proxy.mapFromSource(parent)).data(), expectedNewVal);
QCOMPARE(rowRemoveSpy.size(), 1);
QCOMPARE(rowAboutToBeRemoveSpy.size(), 1);
for (const auto &spyArgs : {rowRemoveSpy.takeFirst(),
rowAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxy.mapFromSource(parent));
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::sortPersistentIndex()
{
QStringListModel model(QStringList{QStringLiteral("Alice"), QStringLiteral("Charlie"), QStringLiteral("Bob")});
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QPersistentModelIndex aliceIdx = proxy.index(0, 0);
QPersistentModelIndex bobIdx = proxy.index(0, 2);
QPersistentModelIndex charlieIdx = proxy.index(0, 1);
connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, this, [&aliceIdx, &bobIdx, &charlieIdx](){
QCOMPARE(aliceIdx.row(), 0);
QCOMPARE(aliceIdx.column(), 0);
QCOMPARE(aliceIdx.data().toString(), QStringLiteral("Alice"));
QCOMPARE(bobIdx.row(), 0);
QCOMPARE(bobIdx.column(), 2);
QCOMPARE(bobIdx.data().toString(), QStringLiteral("Bob"));
QCOMPARE(charlieIdx.row(), 0);
QCOMPARE(charlieIdx.column(), 1);
QCOMPARE(charlieIdx.data().toString(), QStringLiteral("Charlie"));
});
connect(&proxy, &QAbstractItemModel::layoutChanged, this, [&aliceIdx, &bobIdx, &charlieIdx](){
QCOMPARE(aliceIdx.row(), 0);
QCOMPARE(aliceIdx.column(), 0);
QCOMPARE(aliceIdx.data().toString(), QStringLiteral("Alice"));
QCOMPARE(bobIdx.row(), 0);
QCOMPARE(bobIdx.column(), 1);
QCOMPARE(bobIdx.data().toString(), QStringLiteral("Bob"));
QCOMPARE(charlieIdx.row(), 0);
QCOMPARE(charlieIdx.column(), 2);
QCOMPARE(charlieIdx.data().toString(), QStringLiteral("Charlie"));
});
model.sort(0);
QCOMPARE(aliceIdx.row(), 0);
QCOMPARE(aliceIdx.column(), 0);
QCOMPARE(aliceIdx.data().toString(), QStringLiteral("Alice"));
QCOMPARE(bobIdx.row(), 0);
QCOMPARE(bobIdx.column(), 1);
QCOMPARE(bobIdx.data().toString(), QStringLiteral("Bob"));
QCOMPARE(charlieIdx.row(), 0);
QCOMPARE(charlieIdx.column(), 2);
QCOMPARE(charlieIdx.data().toString(), QStringLiteral("Charlie"));
}
void tst_QTransposeProxyModel::createPersistentOnLayoutAboutToBeChanged() // QTBUG-93466
{
QStandardItemModel model(3, 1);
for (int row = 0; row < 3; ++row)
model.setData(model.index(row, 0), row, Qt::UserRole);
model.setSortRole(Qt::UserRole);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QList<QPersistentModelIndex> idxList;
QSignalSpy layoutAboutToBeChangedSpy(&proxy, &QAbstractItemModel::layoutAboutToBeChanged);
QSignalSpy layoutChangedSpy(&proxy, &QAbstractItemModel::layoutChanged);
connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, this, [&idxList, &proxy](){
idxList.clear();
for (int row = 0; row < 3; ++row)
idxList << QPersistentModelIndex(proxy.index(0, row));
});
connect(&proxy, &QAbstractItemModel::layoutChanged, this, [&idxList](){
QCOMPARE(idxList.size(), 3);
QCOMPARE(idxList.at(0).row(), 0);
QCOMPARE(idxList.at(0).column(), 1);
QCOMPARE(idxList.at(0).data(Qt::UserRole).toInt(), 0);
QCOMPARE(idxList.at(1).row(), 0);
QCOMPARE(idxList.at(1).column(), 0);
QCOMPARE(idxList.at(1).data(Qt::UserRole).toInt(), -1);
QCOMPARE(idxList.at(2).row(), 0);
QCOMPARE(idxList.at(2).column(), 2);
QCOMPARE(idxList.at(2).data(Qt::UserRole).toInt(), 2);
});
model.setData(model.index(1, 0), -1, Qt::UserRole);
model.sort(0);
QCOMPARE(layoutAboutToBeChangedSpy.size(), 1);
QCOMPARE(layoutChangedSpy.size(), 1);
}
void tst_QTransposeProxyModel::insertColumnBase_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<QModelIndex>("parent");
QTest::newRow("Table") << createTableModel(this) << QModelIndex();
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << QModelIndex();
QAbstractItemModel *model = createTreeModel(this);
QTest::newRow("Tree_Child_Item") << model << model->index(0, 0);
}
void tst_QTransposeProxyModel::insertColumnBase()
{
QFETCH(QAbstractItemModel * const, model);
QFETCH(const QModelIndex, parent);
QTransposeProxyModel proxy;
QSignalSpy rowInsertSpy(&proxy, &QAbstractItemModel::rowsInserted);
QVERIFY(rowInsertSpy.isValid());
QSignalSpy rowAboutToBeInsertSpy(&proxy, &QAbstractItemModel::rowsAboutToBeInserted);
QVERIFY(rowAboutToBeInsertSpy.isValid());
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
const int oldRowCount = proxy.rowCount(proxy.mapFromSource(parent));
QVERIFY(model->insertColumn(1, parent));
QCOMPARE(proxy.rowCount(proxy.mapFromSource(parent)), oldRowCount + 1);
QVERIFY(!proxy.index(1, 0, proxy.mapFromSource(parent)).data().isValid());
QCOMPARE(rowInsertSpy.size(), 1);
QCOMPARE(rowAboutToBeInsertSpy.size(), 1);
for (const auto &spyArgs : {rowInsertSpy.takeFirst(),
rowAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxy.mapFromSource(parent));
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::removeRowBase_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<QModelIndex>("parent");
QTest::newRow("List") << createListModel(this) << QModelIndex();
QTest::newRow("Table") << createTableModel(this) << QModelIndex();
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << QModelIndex();
QAbstractItemModel *model = createTreeModel(this);
QTest::newRow("Tree_Child_Item") << model << model->index(0, 0);
}
void tst_QTransposeProxyModel::removeRowBase()
{
QFETCH(QAbstractItemModel * const, model);
QFETCH(const QModelIndex, parent);
QTransposeProxyModel proxy;
QSignalSpy columnsRemoveSpy(&proxy, &QAbstractItemModel::columnsRemoved);
QVERIFY(columnsRemoveSpy.isValid());
QSignalSpy columnsAboutToBeRemoveSpy(&proxy, &QAbstractItemModel::columnsAboutToBeRemoved);
QVERIFY(columnsAboutToBeRemoveSpy.isValid());
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
const int oldColCount = proxy.columnCount(proxy.mapFromSource(parent));
const QVariant expectedNewVal = model->index(2, 0, parent).data();
QVERIFY(model->removeRow(1, parent));
QCOMPARE(proxy.columnCount(proxy.mapFromSource(parent)), oldColCount - 1);
QCOMPARE(proxy.index(0, 1, proxy.mapFromSource(parent)).data(), expectedNewVal);
QCOMPARE(columnsRemoveSpy.size(), 1);
QCOMPARE(columnsAboutToBeRemoveSpy.size(), 1);
for (const auto &spyArgs : {columnsRemoveSpy.takeFirst(),
columnsAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxy.mapFromSource(parent));
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::insertRowBase_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<QModelIndex>("parent");
QTest::newRow("List") << createListModel(this) << QModelIndex();
QTest::newRow("Table") << createTableModel(this) << QModelIndex();
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << QModelIndex();
QAbstractItemModel *model = createTreeModel(this);
QTest::newRow("Tree_Child_Item") << model << model->index(0, 0);
}
void tst_QTransposeProxyModel::insertRowBase()
{
QFETCH(QAbstractItemModel * const, model);
QFETCH(const QModelIndex, parent);
QTransposeProxyModel proxy;
QSignalSpy columnsInsertSpy(&proxy, &QAbstractItemModel::columnsInserted);
QVERIFY(columnsInsertSpy.isValid());
QSignalSpy columnsAboutToBeInsertSpy(&proxy, &QAbstractItemModel::columnsAboutToBeInserted);
QVERIFY(columnsAboutToBeInsertSpy.isValid());
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
const int oldColCount = proxy.columnCount(proxy.mapFromSource(parent));
QVERIFY(model->insertRow(1, parent));
QCOMPARE(proxy.columnCount(proxy.mapFromSource(parent)), oldColCount + 1);
QVariant result = proxy.index(0, 1, proxy.mapFromSource(parent)).data();
QVERIFY(result.isNull() || (result.metaType().id() == QMetaType::QString && result.toString().isNull()));
QCOMPARE(columnsInsertSpy.size(), 1);
QCOMPARE(columnsAboutToBeInsertSpy.size(), 1);
for (const auto &spyArgs : {columnsInsertSpy.takeFirst(),
columnsAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxy.mapFromSource(parent));
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::removeColumnProxy_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<bool>("rootItem");
QTest::newRow("List") << createListModel(this) << true;
QTest::newRow("Table") << createTableModel(this) << true;
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << true;
QTest::newRow("Tree_Child_Item") << createTreeModel(this) << false;
}
void tst_QTransposeProxyModel::removeColumnProxy()
{
QFETCH(QAbstractItemModel *, model);
QFETCH(bool, rootItem);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
QSignalSpy columnsRemoveSpy(&proxy, &QAbstractItemModel::columnsRemoved);
QVERIFY(columnsRemoveSpy.isValid());
QSignalSpy columnsAboutToBeRemoveSpy(&proxy, &QAbstractItemModel::columnsAboutToBeRemoved);
QVERIFY(columnsAboutToBeRemoveSpy.isValid());
QSignalSpy rowsRemoveSpy(model, &QAbstractItemModel::rowsRemoved);
QVERIFY(rowsRemoveSpy.isValid());
QSignalSpy rowsAboutToBeRemoveSpy(model, &QAbstractItemModel::rowsAboutToBeRemoved);
QVERIFY(rowsAboutToBeRemoveSpy.isValid());
proxy.setSourceModel(model);
const QModelIndex proxyParent = rootItem ? QModelIndex() : proxy.index(0, 1);
const QModelIndex sourceParent = proxy.mapToSource(proxyParent);
const int oldColCount = proxy.columnCount(proxyParent);
const int oldRowCount = model->rowCount(sourceParent);
const QVariant expectedNewVal = proxy.index(0, 2, proxyParent).data();
QVERIFY(proxy.removeColumn(1, proxyParent));
QCOMPARE(proxy.columnCount(proxyParent), oldColCount - 1);
QCOMPARE(model->rowCount(sourceParent), oldRowCount - 1);
QCOMPARE(proxy.index(0, 1, proxyParent).data(), expectedNewVal);
QCOMPARE(model->index(1, 0, sourceParent).data(), expectedNewVal);
QCOMPARE(columnsRemoveSpy.size(), 1);
QCOMPARE(columnsAboutToBeRemoveSpy.size(), 1);
QCOMPARE(rowsRemoveSpy.size(), 1);
QCOMPARE(rowsAboutToBeRemoveSpy.size(), 1);
for (const auto &spyArgs : {columnsRemoveSpy.takeFirst(),
columnsAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxyParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
for (const auto &spyArgs : {rowsRemoveSpy.takeFirst(),
rowsAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), sourceParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::insertColumnProxy_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<bool>("rootItem");
QTest::newRow("List") << createListModel(this) << true;
QTest::newRow("Table") << createTableModel(this) << true;
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << true;
QTest::newRow("Tree_Child_Item") << createTreeModel(this) << false;
}
void tst_QTransposeProxyModel::insertColumnProxy()
{
QFETCH(QAbstractItemModel *, model);
QFETCH(bool, rootItem);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
QSignalSpy columnsInsertSpy(&proxy, &QAbstractItemModel::columnsInserted);
QVERIFY(columnsInsertSpy.isValid());
QSignalSpy columnsAboutToBeInsertSpy(&proxy, &QAbstractItemModel::columnsAboutToBeInserted);
QVERIFY(columnsAboutToBeInsertSpy.isValid());
QSignalSpy rowsInsertSpy(model, &QAbstractItemModel::rowsInserted);
QVERIFY(rowsInsertSpy.isValid());
QSignalSpy rowsAboutToBeInsertSpy(model, &QAbstractItemModel::rowsAboutToBeInserted);
QVERIFY(rowsAboutToBeInsertSpy.isValid());
proxy.setSourceModel(model);
const QModelIndex proxyParent = rootItem ? QModelIndex() : proxy.index(0, 1);
const QModelIndex sourceParent = proxy.mapToSource(proxyParent);
const int oldColCount = proxy.columnCount(proxyParent);
const int oldRowCount = model->rowCount(sourceParent);
QVERIFY(proxy.insertColumn(1, proxyParent));
QCOMPARE(proxy.columnCount(proxyParent), oldColCount + 1);
QCOMPARE(model->rowCount(sourceParent), oldRowCount + 1);
QVariant result = proxy.index(0, 1, proxyParent).data();
QVERIFY(result.isNull() || (result.metaType().id() == QMetaType::QString && result.toString().isNull()));
result = model->index(1, 0, sourceParent).data();
QVERIFY(result.isNull() || (result.metaType().id() == QMetaType::QString && result.toString().isNull()));
QCOMPARE(columnsInsertSpy.size(), 1);
QCOMPARE(columnsAboutToBeInsertSpy.size(), 1);
QCOMPARE(rowsInsertSpy.size(), 1);
QCOMPARE(rowsAboutToBeInsertSpy.size(), 1);
for (const auto &spyArgs : {columnsInsertSpy.takeFirst(),
columnsAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxyParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
for (const auto &spyArgs : {rowsInsertSpy.takeFirst(),
rowsAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), sourceParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::removeRowProxy_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<bool>("rootItem");
QTest::newRow("Table") << createTableModel(this) << true;
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << true;
QTest::newRow("Tree_Child_Item") << createTreeModel(this) << false;
}
void tst_QTransposeProxyModel::removeRowProxy()
{
QFETCH(QAbstractItemModel *, model);
QFETCH(bool, rootItem);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
QSignalSpy rowsRemoveSpy(&proxy, &QAbstractItemModel::rowsRemoved);
QVERIFY(rowsRemoveSpy.isValid());
QSignalSpy rowsAboutToBeRemoveSpy(&proxy, &QAbstractItemModel::rowsAboutToBeRemoved);
QVERIFY(rowsAboutToBeRemoveSpy.isValid());
QSignalSpy columnsRemoveSpy(model, &QAbstractItemModel::columnsRemoved);
QVERIFY(columnsRemoveSpy.isValid());
QSignalSpy columnsAboutToBeRemoveSpy(model, &QAbstractItemModel::columnsAboutToBeRemoved);
QVERIFY(columnsAboutToBeRemoveSpy.isValid());
proxy.setSourceModel(model);
const QModelIndex proxyParent = rootItem ? QModelIndex() : proxy.index(0, 1);
const QModelIndex sourceParent = proxy.mapToSource(proxyParent);
const int oldRowCount = proxy.rowCount(proxyParent);
const int oldColCount = model->columnCount(sourceParent);
const QVariant expectedNewVal = proxy.index(2, 0, proxyParent).data();
QVERIFY(proxy.removeRow(1, proxyParent));
QCOMPARE(proxy.rowCount(proxyParent), oldRowCount - 1);
QCOMPARE(model->columnCount(sourceParent), oldColCount - 1);
QCOMPARE(proxy.index(1, 0, proxyParent).data(), expectedNewVal);
QCOMPARE(model->index(0, 1, sourceParent).data(), expectedNewVal);
QCOMPARE(columnsRemoveSpy.size(), 1);
QCOMPARE(columnsAboutToBeRemoveSpy.size(), 1);
QCOMPARE(rowsRemoveSpy.size(), 1);
QCOMPARE(rowsAboutToBeRemoveSpy.size(), 1);
for (const auto &spyArgs : {columnsRemoveSpy.takeFirst(),
columnsAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), sourceParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
for (const auto &spyArgs : {rowsRemoveSpy.takeFirst(),
rowsAboutToBeRemoveSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxyParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::insertRowProxy_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<bool>("rootItem");
QTest::newRow("Table") << createTableModel(this) << true;
QTest::newRow("Tree_Root_Item") << createTreeModel(this) << true;
QTest::newRow("Tree_Child_Item") << createTreeModel(this) << false;
}
void tst_QTransposeProxyModel::insertRowProxy()
{
QFETCH(QAbstractItemModel *, model);
QFETCH(bool, rootItem);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
QSignalSpy rowsInsertSpy(&proxy, &QAbstractItemModel::rowsInserted);
QVERIFY(rowsInsertSpy.isValid());
QSignalSpy rowsAboutToBeInsertSpy(&proxy, &QAbstractItemModel::rowsAboutToBeInserted);
QVERIFY(rowsAboutToBeInsertSpy.isValid());
QSignalSpy columnsInsertSpy(model, &QAbstractItemModel::columnsInserted);
QVERIFY(columnsInsertSpy.isValid());
QSignalSpy columnsAboutToBeInsertSpy(model, &QAbstractItemModel::columnsAboutToBeInserted);
QVERIFY(columnsAboutToBeInsertSpy.isValid());
proxy.setSourceModel(model);
const QModelIndex proxyParent = rootItem ? QModelIndex() : proxy.index(0, 1);
const QModelIndex sourceParent = proxy.mapToSource(proxyParent);
const int oldRowCount = proxy.rowCount(proxyParent);
const int oldColCount = model->columnCount(sourceParent);
QVERIFY(proxy.insertRow(1, proxyParent));
QCOMPARE(proxy.rowCount(proxyParent), oldRowCount + 1);
QCOMPARE(model->columnCount(sourceParent), oldColCount + 1);
QVERIFY(proxy.index(1, 0, proxyParent).data().isNull());
QVERIFY(model->index(0, 1, sourceParent).data().isNull());
QCOMPARE(columnsInsertSpy.size(), 1);
QCOMPARE(columnsAboutToBeInsertSpy.size(), 1);
QCOMPARE(rowsInsertSpy.size(), 1);
QCOMPARE(rowsAboutToBeInsertSpy.size(), 1);
for (const auto &spyArgs : {columnsInsertSpy.takeFirst(),
columnsAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), sourceParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
for (const auto &spyArgs : {rowsInsertSpy.takeFirst(),
rowsAboutToBeInsertSpy.takeFirst()}) {
QCOMPARE(spyArgs.at(0).value<QModelIndex>(), proxyParent);
QCOMPARE(spyArgs.at(1).toInt(), 1);
QCOMPARE(spyArgs.at(2).toInt(), 1);
}
delete model;
}
void tst_QTransposeProxyModel::headerData()
{
QStandardItemModel model;
model.insertRows(0, 3);
model.insertColumns(0, 5);
for (int i = 0; i < model.rowCount(); ++i)
model.setHeaderData(i, Qt::Horizontal, QChar('A' + i));
for (int i = 1; i <= model.columnCount(); ++i)
model.setHeaderData(i, Qt::Vertical, i);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
for (int i = 0; i < model.rowCount(); ++i)
QCOMPARE(model.headerData(i, Qt::Horizontal), proxy.headerData(i, Qt::Vertical));
for (int i = 0; i < model.columnCount(); ++i)
QCOMPARE(model.headerData(i, Qt::Vertical), proxy.headerData(i, Qt::Horizontal));
}
void tst_QTransposeProxyModel::setHeaderData()
{
QStandardItemModel model;
model.insertRows(0, 3);
model.insertColumns(0, 5);
for (int i = 0; i < model.rowCount(); ++i)
model.setHeaderData(i, Qt::Horizontal, QChar('A' + i));
for (int i = 1; i <= model.columnCount(); ++i)
model.setHeaderData(i, Qt::Vertical, i);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QVERIFY(proxy.setHeaderData(1, Qt::Horizontal, 99));
QCOMPARE(model.headerData(1, Qt::Vertical).toInt(), 99);
QVERIFY(proxy.setHeaderData(1, Qt::Vertical, QChar('Z')));
QCOMPARE(model.headerData(1, Qt::Horizontal).toChar(), QChar('Z'));
}
void tst_QTransposeProxyModel::span()
{
class SpanModel : public QStandardItemModel
{
Q_DISABLE_COPY(SpanModel)
public:
SpanModel(int rows, int columns, QObject *parent = nullptr)
: QStandardItemModel(rows, columns, parent)
{}
QSize span(const QModelIndex &index) const override
{
Q_UNUSED(index);
return QSize(2, 1);
}
};
SpanModel model(3, 5);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
QCOMPARE(proxy.span(proxy.index(0, 0)), QSize(1, 2));
}
void tst_QTransposeProxyModel::itemData()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QMap<int, QVariant> itmData = proxy.itemData(proxy.index(0, 1));
QCOMPARE(itmData.value(Qt::DisplayRole).toString(), QStringLiteral("1,0"));
QCOMPARE(itmData.value(Qt::UserRole).toInt(), 1);
QCOMPARE(itmData.value(Qt::UserRole + 1).toInt(), 0);
itmData = proxy.itemData(proxy.index(1, 2, proxy.index(0, 1)));
QCOMPARE(itmData.value(Qt::DisplayRole).toString(), QStringLiteral("1,0,2,1"));
QCOMPARE(itmData.value(Qt::UserRole).toInt(), 1);
QCOMPARE(itmData.value(Qt::UserRole + 1).toInt(), 0);
QCOMPARE(itmData.value(Qt::UserRole + 2).toInt(), 2);
QCOMPARE(itmData.value(Qt::UserRole + 3).toInt(), 1);
QVERIFY(proxy.itemData(QModelIndex()).isEmpty());
delete model;
}
void tst_QTransposeProxyModel::setItemData()
{
QAbstractItemModel *model = createTreeModel(this);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QSignalSpy sourceDataChangeSpy(model, &QAbstractItemModel::dataChanged);
QVERIFY(sourceDataChangeSpy.isValid());
QSignalSpy proxyDataChangeSpy(&proxy, &QAbstractItemModel::dataChanged);
QVERIFY(proxyDataChangeSpy.isValid());
const QMap<int, QVariant> itmData = {
std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("Test")),
std::make_pair<int, QVariant>(Qt::UserRole, 88),
std::make_pair<int, QVariant>(Qt::UserRole + 1, 99),
};
QModelIndex idx = proxy.index(0, 1);
QVERIFY(proxy.setItemData(idx, itmData));
QCOMPARE(idx.data(Qt::DisplayRole).toString(), QStringLiteral("Test"));
QCOMPARE(idx.data(Qt::UserRole).toInt(), 88);
QCOMPARE(idx.data(Qt::UserRole + 1).toInt(), 99);
QCOMPARE(sourceDataChangeSpy.size(), 1);
QCOMPARE(proxyDataChangeSpy.size(), 1);
auto signalData = proxyDataChangeSpy.takeFirst();
QCOMPARE(signalData.at(0).value<QModelIndex>(), idx);
QCOMPARE(signalData.at(1).value<QModelIndex>(), idx);
const QList<int> expectedRoles{Qt::DisplayRole, Qt::UserRole, Qt::EditRole, Qt::UserRole + 1};
QList<int> receivedRoles = signalData.at(2).value<QList<int> >();
QCOMPARE(receivedRoles.size(), expectedRoles.size());
for (int role : expectedRoles)
QVERIFY(receivedRoles.contains(role));
signalData = sourceDataChangeSpy.takeFirst();
QCOMPARE(signalData.at(0).value<QModelIndex>(), proxy.mapToSource(idx));
QCOMPARE(signalData.at(1).value<QModelIndex>(), proxy.mapToSource(idx));
receivedRoles = signalData.at(2).value<QList<int> >();
QCOMPARE(receivedRoles.size(), expectedRoles.size());
for (int role : expectedRoles)
QVERIFY(receivedRoles.contains(role));
idx = proxy.index(1, 2, proxy.index(0, 1));
QVERIFY(proxy.setItemData(idx, itmData));
QCOMPARE(idx.data(Qt::DisplayRole).toString(), QStringLiteral("Test"));
QCOMPARE(idx.data(Qt::UserRole).toInt(), 88);
QCOMPARE(idx.data(Qt::UserRole + 1).toInt(), 99);
QCOMPARE(idx.data(Qt::UserRole + 2).toInt(), 2);
QCOMPARE(idx.data(Qt::UserRole + 3).toInt(), 1);
QCOMPARE(sourceDataChangeSpy.size(), 1);
QCOMPARE(proxyDataChangeSpy.size(), 1);
signalData = proxyDataChangeSpy.takeFirst();
QCOMPARE(signalData.at(0).value<QModelIndex>(), idx);
QCOMPARE(signalData.at(1).value<QModelIndex>(), idx);
receivedRoles = signalData.at(2).value<QList<int> >();
QCOMPARE(receivedRoles.size(), expectedRoles.size());
for (int role : expectedRoles)
QVERIFY(receivedRoles.contains(role));
signalData = sourceDataChangeSpy.takeFirst();
QCOMPARE(signalData.at(0).value<QModelIndex>(), proxy.mapToSource(idx));
QCOMPARE(signalData.at(1).value<QModelIndex>(), proxy.mapToSource(idx));
receivedRoles = signalData.at(2).value<QList<int> >();
QCOMPARE(receivedRoles.size(), expectedRoles.size());
for (int role : expectedRoles)
QVERIFY(receivedRoles.contains(role));
QVERIFY(!proxy.setItemData(QModelIndex(), itmData));
delete model;
}
void tst_QTransposeProxyModel::moveRowsBase()
{
QStringListModel model{QStringList{"A", "B", "C", "D"}};
QTransposeProxyModel proxy;
QSignalSpy columnsMoveSpy(&proxy, &QAbstractItemModel::columnsMoved);
QVERIFY(columnsMoveSpy.isValid());
QSignalSpy columnsAboutToBeMoveSpy(&proxy, &QAbstractItemModel::columnsAboutToBeMoved);
QVERIFY(columnsAboutToBeMoveSpy.isValid());
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(&model);
const QStringList expectedNewVal = {"B", "A", "C", "D"};
QVERIFY(model.moveRows(QModelIndex(), 0, 1, QModelIndex(), 2));
for (int i = 0; i < expectedNewVal.size(); ++i)
QCOMPARE(proxy.index(0, i).data(), expectedNewVal.at(i));
QCOMPARE(columnsMoveSpy.size(), 1);
QCOMPARE(columnsAboutToBeMoveSpy.size(), 1);
for (const auto &spyArgs : {columnsMoveSpy.takeFirst(),
columnsAboutToBeMoveSpy.takeFirst()}) {
QVERIFY(!spyArgs.at(0).value<QModelIndex>().isValid());
QCOMPARE(spyArgs.at(1).toInt(), 0);
QCOMPARE(spyArgs.at(2).toInt(), 0);
QVERIFY(!spyArgs.at(3).value<QModelIndex>().isValid());
QCOMPARE(spyArgs.at(4).toInt(), 2);
}
}
void tst_QTransposeProxyModel::moveColumnsProxy()
{
QStringListModel model{QStringList{"A", "B", "C", "D"}};
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
QSignalSpy columnsMoveSpy(&proxy, &QAbstractItemModel::columnsMoved);
QVERIFY(columnsMoveSpy.isValid());
QSignalSpy columnsAboutToBeMoveSpy(&proxy, &QAbstractItemModel::columnsAboutToBeMoved);
QVERIFY(columnsAboutToBeMoveSpy.isValid());
QSignalSpy rowsMoveSpy(&model, &QAbstractItemModel::rowsMoved);
QVERIFY(rowsMoveSpy.isValid());
QSignalSpy rowsAboutToBeMoveSpy(&model, &QAbstractItemModel::rowsAboutToBeMoved);
QVERIFY(rowsAboutToBeMoveSpy.isValid());
proxy.setSourceModel(&model);
const QStringList expectedNewVal = {"B", "A", "C", "D"};
QVERIFY(proxy.moveColumns(QModelIndex(), 0, 1, QModelIndex(), 2));
for (int i = 0; i < expectedNewVal.size(); ++i)
QCOMPARE(proxy.index(0, i).data(), expectedNewVal.at(i));
for (int i = 0; i < expectedNewVal.size(); ++i)
QCOMPARE(model.index(i, 0).data(), expectedNewVal.at(i));
QCOMPARE(columnsMoveSpy.size(), 1);
QCOMPARE(columnsAboutToBeMoveSpy.size(), 1);
QCOMPARE(rowsMoveSpy.size(), 1);
QCOMPARE(rowsAboutToBeMoveSpy.size(), 1);
for (const auto &spyArgs : {columnsMoveSpy.takeFirst(),
columnsAboutToBeMoveSpy.takeFirst(),
rowsMoveSpy.takeFirst(),rowsAboutToBeMoveSpy.takeFirst()}) {
QVERIFY(!spyArgs.at(0).value<QModelIndex>().isValid());
QCOMPARE(spyArgs.at(1).toInt(), 0);
QCOMPARE(spyArgs.at(2).toInt(), 0);
QVERIFY(!spyArgs.at(3).value<QModelIndex>().isValid());
}
}
void tst_QTransposeProxyModel::setData_data()
{
QTest::addColumn<QAbstractItemModel *>("model");
QTest::addColumn<bool>("rootItem");
QTest::addColumn<bool>("viaProxy");
QTest::newRow("List_via_Base") << createListModel(this) << true << false;
QTest::newRow("Table_via_Base") << createTableModel(this) << true << false;
QTest::newRow("Tree_via_Base_Root_Item") << createTreeModel(this) << true << false;
QTest::newRow("Tree_via_Base_Child_Item") << createTreeModel(this) << false << false;
QTest::newRow("List_via_Proxy") << createListModel(this) << true << true;
QTest::newRow("Table_via_Proxy") << createTableModel(this) << true << true;
QTest::newRow("Tree_via_Proxy_Root_Item") << createTreeModel(this) << true << true;
QTest::newRow("Tree_via_Proxy_Child_Item") << createTreeModel(this) << false << true;
}
void tst_QTransposeProxyModel::setData()
{
QFETCH(QAbstractItemModel *, model);
QFETCH(bool, rootItem);
QFETCH(bool, viaProxy);
QTransposeProxyModel proxy;
new QAbstractItemModelTester(&proxy, &proxy);
proxy.setSourceModel(model);
QSignalSpy sourceDataChangeSpy(model, &QAbstractItemModel::dataChanged);
QVERIFY(sourceDataChangeSpy.isValid());
QSignalSpy proxyDataChangeSpy(&proxy, &QAbstractItemModel::dataChanged);
QVERIFY(proxyDataChangeSpy.isValid());
const QString testData = QStringLiteral("TestingSetData");
if (viaProxy) {
const QModelIndex parIdx = rootItem ? QModelIndex() : proxy.index(0, 1);
QVERIFY(proxy.setData(proxy.index(0, 1, parIdx), testData));
QCOMPARE(model->index(1, 0, proxy.mapToSource(parIdx)).data().toString(), testData);
} else {
const QModelIndex parIdx = rootItem ? QModelIndex() : model->index(1, 0);
QVERIFY(model->setData(model->index(1, 0, parIdx), testData));
QCOMPARE(proxy.index(0, 1, proxy.mapFromSource(parIdx)).data().toString(), testData);
}
QCOMPARE(sourceDataChangeSpy.size(), 1);
QCOMPARE(proxyDataChangeSpy.size(), 1);
delete model;
}
QTEST_GUILESS_MAIN(tst_QTransposeProxyModel)
#include "tst_qtransposeproxymodel.moc"