File indexing completed on 2024-03-03 04:04:22

0001 /*
0002     SPDX-FileCopyrightText: 2015 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
0003     SPDX-FileContributor: David Faure <david.faure@kdab.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include <QItemSelectionModel>
0009 #include <QSignalSpy>
0010 #include <QSortFilterProxyModel>
0011 #include <QStandardItemModel>
0012 #include <QTest>
0013 #include <QTreeView>
0014 
0015 #include "dynamictreemodel.h"
0016 
0017 #include "test_model_helpers.h"
0018 #include <kextracolumnsproxymodel.h>
0019 using namespace TestModelHelpers;
0020 
0021 Q_DECLARE_METATYPE(QModelIndex)
0022 
0023 class tst_KExtraColumnsProxyModel : public QObject
0024 {
0025     Q_OBJECT
0026 
0027 private:
0028     class NoExtraColumns : public KExtraColumnsProxyModel
0029     {
0030         QVariant extraColumnData(const QModelIndex &, int, int, int) const override
0031         {
0032             Q_ASSERT(0);
0033             return QVariant();
0034         }
0035     };
0036 
0037     class TwoExtraColumnsProxyModel : public KExtraColumnsProxyModel
0038     {
0039     public:
0040         TwoExtraColumnsProxyModel()
0041             : KExtraColumnsProxyModel()
0042             , m_extraColumnData('Z')
0043         {
0044             appendColumn(QStringLiteral("H5"));
0045             appendColumn(QStringLiteral("WRONG")); // removed two lines below, just to test removeColumn
0046             appendColumn(QStringLiteral("H6"));
0047             removeExtraColumn(1);
0048         }
0049         QVariant extraColumnData(const QModelIndex &, int row, int extraColumn, int role) const override
0050         {
0051             if (role != Qt::DisplayRole) {
0052                 return QVariant();
0053             }
0054             switch (extraColumn) {
0055             case 0:
0056                 return QString(m_extraColumnData);
0057             case 1:
0058                 return QString::number(row);
0059             default:
0060                 Q_ASSERT(0);
0061                 return QVariant();
0062             }
0063         }
0064         bool setExtraColumnData(const QModelIndex &parent, int row, int extraColumn, const QVariant &data, int role) override
0065         {
0066             if (extraColumn == 0 && role == Qt::EditRole) {
0067                 m_extraColumnData = data.toString().at(0);
0068                 extraColumnDataChanged(QModelIndex(), 0, extraColumn, QList<int>() << Qt::EditRole);
0069                 return true;
0070             }
0071             return KExtraColumnsProxyModel::setExtraColumnData(parent, row, extraColumn, data, role);
0072         }
0073         void changeExtraColumnData()
0074         {
0075             m_extraColumnData = '<';
0076             extraColumnDataChanged(QModelIndex(), 0, 0, QList<int>() << Qt::EditRole);
0077         }
0078 
0079     private:
0080         QChar m_extraColumnData;
0081     };
0082 
0083 private Q_SLOTS:
0084 
0085     void initTestCase()
0086     {
0087         qRegisterMetaType<QModelIndex>();
0088     }
0089 
0090     void init()
0091     {
0092         // Prepare the source model to use later on
0093         mod.clear();
0094         mod.appendRow(makeStandardItems(QStringList() << QStringLiteral("A") << QStringLiteral("B") << QStringLiteral("C") << QStringLiteral("D")));
0095         mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("m") << QStringLiteral("n") << QStringLiteral("o") << QStringLiteral("p")));
0096         mod.item(0, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("q") << QStringLiteral("r") << QStringLiteral("s") << QStringLiteral("t")));
0097         mod.item(0, 0)->child(1, 0)->appendRow(
0098             makeStandardItems(QStringList() << QStringLiteral("u") << QStringLiteral("v") << QStringLiteral("w") << QStringLiteral("w")));
0099         mod.appendRow(makeStandardItems(QStringList() << QStringLiteral("E") << QStringLiteral("F") << QStringLiteral("G") << QStringLiteral("H")));
0100         mod.item(1, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("x") << QStringLiteral("y") << QStringLiteral("z") << QStringLiteral(".")));
0101         mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3") << QStringLiteral("H4"));
0102 
0103         QCOMPARE(extractRowTexts(&mod, 0), QStringLiteral("ABCD"));
0104         QCOMPARE(extractRowTexts(&mod, 0, mod.index(0, 0)), QStringLiteral("mnop"));
0105         QCOMPARE(extractRowTexts(&mod, 1, mod.index(0, 0)), QStringLiteral("qrst"));
0106         QCOMPARE(extractRowTexts(&mod, 0, mod.index(1, 0, mod.index(0, 0))), QStringLiteral("uvww"));
0107         QCOMPARE(extractRowTexts(&mod, 1), QStringLiteral("EFGH"));
0108         QCOMPARE(extractRowTexts(&mod, 0, mod.index(1, 0)), QStringLiteral("xyz."));
0109         QCOMPARE(extractHorizontalHeaderTexts(&mod), QStringLiteral("H1H2H3H4"));
0110     }
0111 
0112     void shouldDoNothingIfNoExtraColumns()
0113     {
0114         // Given a extra-columns proxy
0115         NoExtraColumns pm;
0116 
0117         // When setting it to a source model
0118         pm.setSourceModel(&mod);
0119 
0120         // Then the proxy should show the same as the model
0121         QCOMPARE(pm.rowCount(), mod.rowCount());
0122         QCOMPARE(pm.columnCount(), mod.columnCount());
0123 
0124         QCOMPARE(pm.rowCount(pm.index(0, 0)), 2);
0125         QCOMPARE(pm.index(0, 0).parent(), QModelIndex());
0126 
0127         // (verify that the mapFromSource(mapToSource(x)) == x roundtrip works)
0128         for (int row = 0; row < pm.rowCount(); ++row) {
0129             for (int col = 0; col < pm.columnCount(); ++col) {
0130                 QCOMPARE(pm.mapFromSource(pm.mapToSource(pm.index(row, col))), pm.index(row, col));
0131             }
0132         }
0133 
0134         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD"));
0135         QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnop"));
0136         QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrst"));
0137         QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGH"));
0138         QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz."));
0139         QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H4"));
0140     }
0141 
0142     void shouldShowExtraColumns()
0143     {
0144         // Given a extra-columns proxy with two extra columns
0145         TwoExtraColumnsProxyModel pm;
0146 
0147         // When setting it to a source model
0148         pm.setSourceModel(&mod);
0149 
0150         // Then the proxy should show the extra column
0151         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCDZ0"));
0152         QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnopZ0"));
0153         QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrstZ1"));
0154         QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGHZ1"));
0155         QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz.Z0"));
0156         QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H4H5H6"));
0157 
0158         // Verify tree structure of proxy
0159         const QModelIndex secondParent = pm.index(1, 0);
0160         QVERIFY(!secondParent.parent().isValid());
0161         QCOMPARE(indexToText(pm.index(0, 0, secondParent).parent()), indexToText(secondParent));
0162         QCOMPARE(indexToText(pm.index(0, 3, secondParent).parent()), indexToText(secondParent));
0163         QVERIFY(indexToText(pm.index(0, 4)).startsWith(QLatin1String("0,4,")));
0164         QCOMPARE(indexToText(pm.index(0, 4, secondParent).parent()), indexToText(secondParent));
0165         QVERIFY(indexToText(pm.index(0, 5)).startsWith(QLatin1String("0,5,")));
0166         QCOMPARE(indexToText(pm.index(0, 5, secondParent).parent()), indexToText(secondParent));
0167 
0168         QCOMPARE(pm.index(0, 0).sibling(0, 4).column(), 4);
0169         QCOMPARE(pm.index(0, 4).sibling(0, 1).column(), 1);
0170 
0171         QCOMPARE(pm.buddy(pm.index(0, 0)), pm.index(0, 0));
0172         QCOMPARE(pm.buddy(pm.index(0, 4)), pm.index(0, 4));
0173 
0174         QVERIFY(pm.hasChildren(pm.index(0, 0)));
0175         QVERIFY(!pm.hasChildren(pm.index(0, 4)));
0176 
0177         QVERIFY(!pm.canFetchMore(QModelIndex()));
0178     }
0179 
0180     void shouldHandleDataChanged()
0181     {
0182         // Given a extra-columns proxy, with two extra columns
0183         TwoExtraColumnsProxyModel pm;
0184         setup(pm);
0185         QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex, QModelIndex)));
0186 
0187         // When a cell in a source model changes
0188         mod.item(0, 2)->setData("c", Qt::EditRole);
0189 
0190         // Then the change should be notified to the proxy
0191         QCOMPARE(dataChangedSpy.count(), 1);
0192         QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 2));
0193         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABcDZ0"));
0194     }
0195 
0196     void shouldHandleDataChangedInExtraColumn()
0197     {
0198         // Given a extra-columns proxy, with two extra columns
0199         TwoExtraColumnsProxyModel pm;
0200         setup(pm);
0201         QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex, QModelIndex)));
0202 
0203         // When the proxy wants to signal a change in an extra column
0204         pm.changeExtraColumnData();
0205 
0206         // Then the change should be available and notified
0207         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD<0"));
0208         QCOMPARE(dataChangedSpy.count(), 1);
0209         QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 4));
0210     }
0211 
0212     void shouldHandleSetDataInNormalColumn()
0213     {
0214         // Given a extra-columns proxy, with two extra columns
0215         TwoExtraColumnsProxyModel pm;
0216         setup(pm);
0217         QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex, QModelIndex)));
0218 
0219         // When editing a cell in the proxy
0220         QVERIFY(pm.setData(pm.index(0, 2), "c", Qt::EditRole));
0221 
0222         // Then the change should be available and notified
0223         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABcDZ0"));
0224         QCOMPARE(dataChangedSpy.count(), 1);
0225         QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 2));
0226     }
0227 
0228     void shouldHandleSetDataInExtraColumn()
0229     {
0230         // Given a extra-columns proxy, with two extra columns
0231         TwoExtraColumnsProxyModel pm;
0232         setup(pm);
0233         QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex, QModelIndex)));
0234 
0235         // When editing a cell in the proxy
0236         QVERIFY(pm.setData(pm.index(0, 4), "-", Qt::EditRole));
0237 
0238         // Then the change should be available and notified
0239         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD-0"));
0240         QCOMPARE(dataChangedSpy.count(), 1);
0241         QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 4));
0242     }
0243 
0244     void shouldHandleRowInsertion()
0245     {
0246         // Given a extra-columns proxy, with two extra columns
0247         TwoExtraColumnsProxyModel pm;
0248         setup(pm);
0249 
0250         QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex, int, int)));
0251         QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex, int, int)));
0252 
0253         // When a source model inserts a new (child) row
0254         mod.item(1, 0)->appendRow(makeStandardItems(QStringList() << QStringLiteral("1") << QStringLiteral("2") << QStringLiteral("3") << QStringLiteral("4")));
0255 
0256         // Then the proxy should notify its users and show changes
0257         QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("1,1"));
0258         QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("1,1"));
0259         QCOMPARE(pm.rowCount(), 2);
0260         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCDZ0"));
0261         QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnopZ0"));
0262         QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrstZ1"));
0263         QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGHZ1"));
0264         QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz.Z0"));
0265         QCOMPARE(extractRowTexts(&pm, 1, pm.index(1, 0)), QStringLiteral("1234Z1"));
0266         QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H4H5H6"));
0267     }
0268 
0269     void shouldHandleColumnInsertion()
0270     {
0271         // Given a extra-columns proxy, with two extra columns
0272         TwoExtraColumnsProxyModel pm;
0273         setup(pm);
0274 
0275         QCOMPARE(pm.columnCount(), 6);
0276         QCOMPARE(mod.columnCount(), 4);
0277 
0278         QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex, int, int)));
0279         QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex, int, int)));
0280 
0281         // When a source model inserts a new column
0282         mod.setColumnCount(5); // like QStandardItem::setChild does
0283         QCOMPARE(mod.columnCount(), 5);
0284         // QStandardItemModel is quite dumb, it records the number of columns in each item
0285         for (int row = 0; row < mod.rowCount(); ++row) {
0286             mod.item(row, 0)->setColumnCount(5);
0287         }
0288 
0289         // Then the proxy should notify its users and show changes
0290         QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("4,4;4,4;4,4")); // QStandardItemModel emits it for each parent
0291         QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("4,4;4,4;4,4"));
0292         QCOMPARE(pm.columnCount(), 7);
0293         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCD Z0"));
0294         QCOMPARE(extractRowTexts(&pm, 0, pm.index(0, 0)), QStringLiteral("mnop Z0"));
0295         QCOMPARE(extractRowTexts(&pm, 1, pm.index(0, 0)), QStringLiteral("qrst Z1"));
0296         QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("EFGH Z1"));
0297         QCOMPARE(extractRowTexts(&pm, 0, pm.index(1, 0)), QStringLiteral("xyz. Z0"));
0298         QCOMPARE(extractHorizontalHeaderTexts(&pm), QStringLiteral("H1H2H3H45H5H6")); // '5' was inserted in there
0299     }
0300 
0301     // row removal, layoutChanged, modelReset -> same thing, works via QIdentityProxyModel
0302     // missing: test for mapSelectionToSource
0303     // missing: test for moving a row in an underlying model. Problem: QStandardItemModel doesn't implement moveRow...
0304 
0305     void shouldHandleLayoutChanged()
0306     {
0307         // Given a extra-columns proxy, with two extra columns
0308         TwoExtraColumnsProxyModel pm;
0309         // And a QSFPM underneath
0310         QSortFilterProxyModel proxy;
0311         proxy.setSourceModel(&mod);
0312         QCOMPARE(proxy.columnCount(), 4);
0313         pm.setSourceModel(&proxy);
0314         QCOMPARE(pm.columnCount(), 6);
0315         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABCDZ0"));
0316         // And a selection
0317         QItemSelectionModel selection(&pm);
0318         selection.select(pm.index(0, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
0319         const QModelIndex grandChild = pm.index(0, 0, pm.index(1, 0, pm.index(0, 0)));
0320         QCOMPARE(grandChild.data().toString(), QStringLiteral("u"));
0321         selection.select(grandChild, QItemSelectionModel::Select | QItemSelectionModel::Rows);
0322         const QModelIndexList lst = selection.selectedIndexes();
0323         QCOMPARE(lst.count(), 12);
0324         for (int col = 0; col < 6; ++col) {
0325             QCOMPARE(lst.at(col).row(), 0);
0326             QCOMPARE(lst.at(col).column(), col);
0327         }
0328 
0329         // When sorting
0330         pm.sort(0, Qt::DescendingOrder);
0331 
0332         // Then the proxy should be sorted
0333         QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("EFGHZ0"));
0334         QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("ABCDZ1"));
0335         // And the selection should be updated accordingly
0336         const QModelIndexList lstAfter = selection.selectedIndexes();
0337         QCOMPARE(lstAfter.count(), 12);
0338         for (int col = 0; col < 6; ++col) {
0339             QCOMPARE(lstAfter.at(col).row(), 1);
0340             QCOMPARE(lstAfter.at(col).column(), col);
0341         }
0342     }
0343 
0344     void persistIndexOnLayoutChange()
0345     {
0346         DynamicTreeModel model;
0347 
0348         ModelResetCommand resetCommand(&model);
0349 
0350         resetCommand.setInitialTree(
0351             " - 1"
0352             " - - 2"
0353             " - - - 3"
0354             " - - - - 4"
0355             " - - - - 5");
0356         resetCommand.doCommand();
0357 
0358         NoExtraColumns proxy;
0359         proxy.setSourceModel(&model);
0360 
0361         QPersistentModelIndex persistentIndex;
0362 
0363         QPersistentModelIndex persistentIndexToMove = model.match(model.index(0, 0), Qt::DisplayRole, "4", 1, Qt::MatchRecursive).first();
0364         QCOMPARE(persistentIndexToMove.row(), 0);
0365         QCOMPARE(persistentIndexToMove.parent(), model.match(model.index(0, 0), Qt::DisplayRole, "3", 1, Qt::MatchRecursive).first());
0366 
0367         QPersistentModelIndex sourcePersistentIndex = model.match(model.index(0, 0), Qt::DisplayRole, "5", 1, Qt::MatchRecursive).first();
0368 
0369         QCOMPARE(sourcePersistentIndex.data().toString(), QStringLiteral("5"));
0370 
0371         bool gotLayoutAboutToBeChanged = false;
0372         bool gotLayoutChanged = false;
0373 
0374         QObject::connect(&proxy, &QAbstractItemModel::layoutAboutToBeChanged, &proxy, [&proxy, &persistentIndex, &gotLayoutAboutToBeChanged] {
0375             gotLayoutAboutToBeChanged = true;
0376             persistentIndex = proxy.match(proxy.index(0, 0), Qt::DisplayRole, "5", 1, Qt::MatchRecursive).first();
0377             QCOMPARE(persistentIndex.row(), 1);
0378         });
0379 
0380         QObject::connect(&proxy, &QAbstractItemModel::layoutChanged, &proxy, [&proxy, &persistentIndex, &sourcePersistentIndex, &gotLayoutChanged] {
0381             gotLayoutChanged = true;
0382             QCOMPARE(QModelIndex(persistentIndex), proxy.mapFromSource(sourcePersistentIndex));
0383         });
0384 
0385         ModelMoveLayoutChangeCommand layoutChangeCommand(&model, nullptr);
0386 
0387         layoutChangeCommand.setAncestorRowNumbers({0, 0, 0});
0388         layoutChangeCommand.setStartRow(0);
0389         layoutChangeCommand.setEndRow(0);
0390         layoutChangeCommand.setDestAncestors({0, 0});
0391         layoutChangeCommand.setDestRow(1);
0392 
0393         layoutChangeCommand.doCommand();
0394 
0395         QCOMPARE(persistentIndex.row(), 0);
0396 
0397         QCOMPARE(persistentIndexToMove.row(), 1);
0398         QCOMPARE(persistentIndexToMove.parent(), model.match(model.index(0, 0), Qt::DisplayRole, "2", 1, Qt::MatchRecursive).first());
0399 
0400         QVERIFY(gotLayoutAboutToBeChanged);
0401         QVERIFY(gotLayoutChanged);
0402         QVERIFY(persistentIndex.isValid());
0403     }
0404 
0405 private:
0406     void setup(KExtraColumnsProxyModel &pm)
0407     {
0408         pm.setSourceModel(&mod);
0409     }
0410 
0411     static QString indexToText(const QModelIndex &index)
0412     {
0413         if (!index.isValid()) {
0414             return QStringLiteral("invalid");
0415         }
0416         return QString::number(index.row()) + "," + QString::number(index.column()) + ","
0417             + QString::number(reinterpret_cast<qulonglong>(index.internalPointer()), 16) + " in "
0418             + QString::number(reinterpret_cast<qulonglong>(index.model()), 16);
0419     }
0420 
0421     QStandardItemModel mod;
0422 };
0423 
0424 QTEST_MAIN(tst_KExtraColumnsProxyModel)
0425 
0426 #include "kextracolumnsproxymodeltest.moc"