File indexing completed on 2025-02-16 03:42:55
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"