1/****************************************************************************
2**
3** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author David Faure <david.faure@kdab.com>
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtCore module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include <QSignalSpy>
41#include <QSortFilterProxyModel>
42#include <QTest>
43#include <QStandardItemModel>
44#include <QIdentityProxyModel>
45#include <QItemSelectionModel>
46#include <QMimeData>
47#include <QStringListModel>
48#include <QAbstractItemModelTester>
49
50#include <qconcatenatetablesproxymodel.h>
51
52Q_DECLARE_METATYPE(QModelIndex)
53
54// Extracts a full row from a model as a string
55// Works best if every cell contains only one character
56static QString extractRowTexts(QAbstractItemModel *model, int row, const QModelIndex &parent = QModelIndex())
57{
58 QString result;
59 const int colCount = model->columnCount();
60 for (int col = 0; col < colCount; ++col) {
61 const QString txt = model->index(row, column: col, parent).data().toString();
62 result += txt.isEmpty() ? QStringLiteral(" ") : txt;
63 }
64 return result;
65}
66
67// Extracts a full column from a model as a string
68// Works best if every cell contains only one character
69static QString extractColumnTexts(QAbstractItemModel *model, int column, const QModelIndex &parent = QModelIndex())
70{
71 QString result;
72 const int rowCount = model->rowCount();
73 for (int row = 0; row < rowCount; ++row) {
74 const QString txt = model->index(row, column, parent).data().toString();
75 result += txt.isEmpty() ? QStringLiteral(" ") : txt;
76 }
77 return result;
78}
79
80static QString rowSpyToText(const QSignalSpy &spy)
81{
82 if (!spy.isValid())
83 return QStringLiteral("THE SIGNALSPY IS INVALID!");
84 QString str;
85 for (int i = 0; i < spy.count(); ++i) {
86 str += spy.at(i).at(i: 1).toString() + QLatin1Char(',') + spy.at(i).at(i: 2).toString();
87 if (i + 1 < spy.count())
88 str += QLatin1Char(';');
89 }
90 return str;
91}
92
93class tst_QConcatenateTablesProxyModel : public QObject
94{
95 Q_OBJECT
96
97private Q_SLOTS:
98 void init();
99 void shouldAggregateTwoModelsCorrectly();
100 void shouldAggregateThenRemoveTwoEmptyModelsCorrectly();
101 void shouldAggregateTwoEmptyModelsWhichThenGetFilled();
102 void shouldHandleDataChanged();
103 void shouldHandleSetData();
104 void shouldHandleSetItemData();
105 void shouldHandleRowInsertionAndRemoval();
106 void shouldAggregateAnotherModelThenRemoveModels();
107 void shouldUseSmallestColumnCount();
108 void shouldIncreaseColumnCountWhenRemovingFirstModel();
109 void shouldHandleColumnInsertionAndRemoval();
110 void shouldPropagateLayoutChanged();
111 void shouldReactToModelReset();
112 void shouldUpdateColumnsOnModelReset();
113 void shouldPropagateDropOnItem_data();
114 void shouldPropagateDropOnItem();
115 void shouldPropagateDropBetweenItems();
116 void shouldPropagateDropBetweenItemsAtModelBoundary();
117 void shouldPropagateDropAfterLastRow_data();
118 void shouldPropagateDropAfterLastRow();
119 void qtbug91788();
120
121private:
122 QStandardItemModel mod;
123 QStandardItemModel mod2;
124 QStandardItemModel mod3;
125};
126
127void tst_QConcatenateTablesProxyModel::init()
128{
129 // Prepare some source models to use later on
130 mod.clear();
131 mod.appendRow(items: { new QStandardItem(QStringLiteral("A")), new QStandardItem(QStringLiteral("B")), new QStandardItem(QStringLiteral("C")) });
132 mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
133 mod.setVerticalHeaderLabels(QStringList() << QStringLiteral("One"));
134
135 mod2.clear();
136 mod2.appendRow(items: { new QStandardItem(QStringLiteral("D")), new QStandardItem(QStringLiteral("E")), new QStandardItem(QStringLiteral("F")) });
137 mod2.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
138 mod2.setVerticalHeaderLabels(QStringList() << QStringLiteral("Two"));
139
140 mod3.clear();
141 mod3.appendRow(items: { new QStandardItem(QStringLiteral("1")), new QStandardItem(QStringLiteral("2")), new QStandardItem(QStringLiteral("3")) });
142 mod3.appendRow(items: { new QStandardItem(QStringLiteral("4")), new QStandardItem(QStringLiteral("5")), new QStandardItem(QStringLiteral("6")) });
143}
144
145void tst_QConcatenateTablesProxyModel::shouldAggregateTwoModelsCorrectly()
146{
147 // Given a combining proxy
148 QConcatenateTablesProxyModel pm;
149
150 // When adding two source models
151 pm.addSourceModel(sourceModel: &mod);
152 pm.addSourceModel(sourceModel: &mod2);
153 QAbstractItemModelTester modelTest(&pm, this);
154
155 // Then the proxy should show 2 rows
156 QCOMPARE(pm.rowCount(), 2);
157 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
158 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
159
160 // ... and correct headers
161 QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
162 QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
163 QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
164 QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
165 QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
166
167 QVERIFY(!pm.canFetchMore(QModelIndex()));
168}
169
170void tst_QConcatenateTablesProxyModel::shouldAggregateThenRemoveTwoEmptyModelsCorrectly()
171{
172 // Given a combining proxy
173 QConcatenateTablesProxyModel pm;
174
175 // When adding two empty models
176 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
177 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
178 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
179 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
180 QIdentityProxyModel i1, i2;
181 pm.addSourceModel(sourceModel: &i1);
182 pm.addSourceModel(sourceModel: &i2);
183
184 // Then the proxy should still be empty (and no signals emitted)
185 QCOMPARE(pm.rowCount(), 0);
186 QCOMPARE(pm.columnCount(), 0);
187 QCOMPARE(rowATBISpy.count(), 0);
188 QCOMPARE(rowInsertedSpy.count(), 0);
189
190 // When removing the empty models
191 pm.removeSourceModel(sourceModel: &i1);
192 pm.removeSourceModel(sourceModel: &i2);
193
194 // Then the proxy should still be empty (and no signals emitted)
195 QCOMPARE(pm.rowCount(), 0);
196 QCOMPARE(pm.columnCount(), 0);
197 QCOMPARE(rowATBRSpy.count(), 0);
198 QCOMPARE(rowRemovedSpy.count(), 0);
199}
200
201void tst_QConcatenateTablesProxyModel::shouldAggregateTwoEmptyModelsWhichThenGetFilled()
202{
203 // Given a combining proxy with two empty models
204 QConcatenateTablesProxyModel pm;
205 QIdentityProxyModel i1, i2;
206 pm.addSourceModel(sourceModel: &i1);
207 pm.addSourceModel(sourceModel: &i2);
208
209 // When filling them afterwards
210 i1.setSourceModel(&mod);
211 i2.setSourceModel(&mod2);
212 QAbstractItemModelTester modelTest(&pm, this);
213
214 // Then the proxy should show 2 rows
215 QCOMPARE(pm.rowCount(), 2);
216 QCOMPARE(pm.columnCount(), 3);
217 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
218 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
219
220 // ... and correct headers
221 QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
222 QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
223 QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
224 QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
225 QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
226
227 QVERIFY(!pm.canFetchMore(QModelIndex()));
228}
229
230void tst_QConcatenateTablesProxyModel::shouldHandleDataChanged()
231{
232 // Given two models combined
233 QConcatenateTablesProxyModel pm;
234 pm.addSourceModel(sourceModel: &mod);
235 pm.addSourceModel(sourceModel: &mod2);
236 QAbstractItemModelTester modelTest(&pm, this);
237 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
238
239 // When a cell in a source model changes
240 mod.item(row: 0, column: 0)->setData(value: "a", role: Qt::EditRole);
241
242 // Then the change should be notified to the proxy
243 QCOMPARE(dataChangedSpy.count(), 1);
244 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
245 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
246
247 // Same test with the other model
248 mod2.item(row: 0, column: 2)->setData(value: "f", role: Qt::EditRole);
249
250 QCOMPARE(dataChangedSpy.count(), 2);
251 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
252 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
253}
254
255void tst_QConcatenateTablesProxyModel::shouldHandleSetData()
256{
257 // Given two models combined
258 QConcatenateTablesProxyModel pm;
259 pm.addSourceModel(sourceModel: &mod);
260 pm.addSourceModel(sourceModel: &mod2);
261 QAbstractItemModelTester modelTest(&pm, this);
262 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
263
264 // When changing a cell using setData
265 pm.setData(index: pm.index(row: 0, column: 0), value: "a");
266
267 // Then the change should be notified to the proxy
268 QCOMPARE(dataChangedSpy.count(), 1);
269 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
270 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
271
272 // Same test with the other model
273 pm.setData(index: pm.index(row: 1, column: 2), value: "f");
274
275 QCOMPARE(dataChangedSpy.count(), 2);
276 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
277 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
278}
279
280void tst_QConcatenateTablesProxyModel::shouldHandleSetItemData()
281{
282 // Given two models combined
283 QConcatenateTablesProxyModel pm;
284 pm.addSourceModel(sourceModel: &mod);
285 pm.addSourceModel(sourceModel: &mod2);
286 QAbstractItemModelTester modelTest(&pm, this);
287 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
288
289 // When changing a cell using setData
290 pm.setItemData(index: pm.index(row: 0, column: 0), roles: QMap<int, QVariant>{ std::make_pair<int, QVariant>(x: Qt::DisplayRole, QStringLiteral("X")),
291 std::make_pair<int, QVariant>(x: Qt::UserRole, y: 88) });
292
293 // Then the change should be notified to the proxy
294 QCOMPARE(dataChangedSpy.count(), 1);
295 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
296 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("XBC"));
297 QCOMPARE(pm.index(0, 0).data(Qt::UserRole).toInt(), 88);
298
299 // Same test with the other model
300 pm.setItemData(index: pm.index(row: 1, column: 2), roles: QMap<int, QVariant>{ std::make_pair<int, QVariant>(x: Qt::DisplayRole, QStringLiteral("Y")),
301 std::make_pair<int, QVariant>(x: Qt::UserRole, y: 89) });
302
303 QCOMPARE(dataChangedSpy.count(), 2);
304 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
305 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEY"));
306 QCOMPARE(pm.index(1, 2).data(Qt::UserRole).toInt(), 89);
307}
308
309void tst_QConcatenateTablesProxyModel::shouldHandleRowInsertionAndRemoval()
310{
311 // Given two models combined
312 QConcatenateTablesProxyModel pm;
313 pm.addSourceModel(sourceModel: &mod);
314 pm.addSourceModel(sourceModel: &mod2);
315 QAbstractItemModelTester modelTest(&pm, this);
316 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
317 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
318
319 // When a source model inserts a new row
320 QList<QStandardItem *> row;
321 row.append(t: new QStandardItem(QStringLiteral("1")));
322 row.append(t: new QStandardItem(QStringLiteral("2")));
323 row.append(t: new QStandardItem(QStringLiteral("3")));
324 mod2.insertRow(row: 0, items: row);
325
326 // Then the proxy should notify its users and show changes
327 QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("1,1"));
328 QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("1,1"));
329 QCOMPARE(pm.rowCount(), 3);
330 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
331 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
332 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
333
334 // When removing that row
335 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
336 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
337 mod2.removeRow(arow: 0);
338
339 // Then the proxy should notify its users and show changes
340 QCOMPARE(rowATBRSpy.count(), 1);
341 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
342 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
343 QCOMPARE(rowRemovedSpy.count(), 1);
344 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
345 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
346 QCOMPARE(pm.rowCount(), 2);
347 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
348 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
349
350 // When removing the last row from mod2
351 rowATBRSpy.clear();
352 rowRemovedSpy.clear();
353 mod2.removeRow(arow: 0);
354
355 // Then the proxy should notify its users and show changes
356 QCOMPARE(rowATBRSpy.count(), 1);
357 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
358 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
359 QCOMPARE(rowRemovedSpy.count(), 1);
360 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
361 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
362 QCOMPARE(pm.rowCount(), 1);
363 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
364}
365
366void tst_QConcatenateTablesProxyModel::shouldAggregateAnotherModelThenRemoveModels()
367{
368 // Given two models combined, and a third model
369 QConcatenateTablesProxyModel pm;
370 pm.addSourceModel(sourceModel: &mod);
371 pm.addSourceModel(sourceModel: &mod2);
372 QAbstractItemModelTester modelTest(&pm, this);
373
374 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
375 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
376
377 // When adding the new source model
378 pm.addSourceModel(sourceModel: &mod3);
379
380 // Then the proxy should notify its users about the two rows inserted
381 QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("2,3"));
382 QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("2,3"));
383 QCOMPARE(pm.rowCount(), 4);
384 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
385 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
386 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
387 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
388
389 // When removing that source model again
390 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
391 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
392 pm.removeSourceModel(sourceModel: &mod3);
393
394 // Then the proxy should notify its users about the row removed
395 QCOMPARE(rowATBRSpy.count(), 1);
396 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 2);
397 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 3);
398 QCOMPARE(rowRemovedSpy.count(), 1);
399 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 2);
400 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 3);
401 QCOMPARE(pm.rowCount(), 2);
402 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
403 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
404
405 // When removing model 2
406 rowATBRSpy.clear();
407 rowRemovedSpy.clear();
408 pm.removeSourceModel(sourceModel: &mod2);
409 QCOMPARE(rowATBRSpy.count(), 1);
410 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
411 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
412 QCOMPARE(rowRemovedSpy.count(), 1);
413 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
414 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
415 QCOMPARE(pm.rowCount(), 1);
416 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
417
418 // When removing model 1
419 rowATBRSpy.clear();
420 rowRemovedSpy.clear();
421 pm.removeSourceModel(sourceModel: &mod);
422 QCOMPARE(rowATBRSpy.count(), 1);
423 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 0);
424 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 0);
425 QCOMPARE(rowRemovedSpy.count(), 1);
426 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 0);
427 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 0);
428 QCOMPARE(pm.rowCount(), 0);
429}
430
431void tst_QConcatenateTablesProxyModel::shouldUseSmallestColumnCount()
432{
433 QConcatenateTablesProxyModel pm;
434 pm.addSourceModel(sourceModel: &mod);
435 pm.addSourceModel(sourceModel: &mod2);
436 mod2.setColumnCount(1);
437 pm.addSourceModel(sourceModel: &mod3);
438 QAbstractItemModelTester modelTest(&pm, this);
439
440 QCOMPARE(pm.rowCount(), 4);
441 QCOMPARE(pm.columnCount(), 1);
442 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
443 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
444 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("1"));
445 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("4"));
446
447 const QModelIndex indexA = pm.mapFromSource(sourceIndex: mod.index(row: 0, column: 0));
448 QVERIFY(indexA.isValid());
449 QCOMPARE(indexA, pm.index(0, 0));
450
451 const QModelIndex indexB = pm.mapFromSource(sourceIndex: mod.index(row: 0, column: 1));
452 QVERIFY(!indexB.isValid());
453
454 const QModelIndex indexD = pm.mapFromSource(sourceIndex: mod2.index(row: 0, column: 0));
455 QVERIFY(indexD.isValid());
456 QCOMPARE(indexD, pm.index(1, 0));
457
458 // Test setData in an ignored column (QTBUG-91253)
459 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
460 mod.setData(index: mod.index(row: 0, column: 1), value: "b");
461 QCOMPARE(dataChangedSpy.count(), 0);
462
463 // Test dataChanged across all columns, some visible, some ignored
464 mod.dataChanged(topLeft: mod.index(row: 0, column: 0), bottomRight: mod.index(row: 0, column: 2));
465 QCOMPARE(dataChangedSpy.count(), 1);
466 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
467 QCOMPARE(dataChangedSpy.at(0).at(1).toModelIndex(), pm.index(0, 0));
468}
469
470void tst_QConcatenateTablesProxyModel::shouldIncreaseColumnCountWhenRemovingFirstModel()
471{
472 // Given a model with 2 columns and one with 3 columns
473 QConcatenateTablesProxyModel pm;
474 pm.addSourceModel(sourceModel: &mod);
475 QAbstractItemModelTester modelTest(&pm, this);
476 mod.setColumnCount(2);
477 pm.addSourceModel(sourceModel: &mod2);
478 QCOMPARE(pm.rowCount(), 2);
479 QCOMPARE(pm.columnCount(), 2);
480
481 QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
482 QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
483 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
484 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
485
486 // When removing the first source model
487 pm.removeSourceModel(sourceModel: &mod);
488
489 // Then the proxy should notify its users about the row removed, and the column added
490 QCOMPARE(pm.rowCount(), 1);
491 QCOMPARE(pm.columnCount(), 3);
492 QCOMPARE(rowSpyToText(rowATBRSpy), QStringLiteral("0,0"));
493 QCOMPARE(rowSpyToText(rowRemovedSpy), QStringLiteral("0,0"));
494 QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
495 QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
496 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("DEF"));
497}
498
499void tst_QConcatenateTablesProxyModel::shouldHandleColumnInsertionAndRemoval()
500{
501 // Given two models combined, one with 2 columns and one with 3
502 QConcatenateTablesProxyModel pm;
503 pm.addSourceModel(sourceModel: &mod);
504 QAbstractItemModelTester modelTest(&pm, this);
505 mod.setColumnCount(2);
506 pm.addSourceModel(sourceModel: &mod2);
507 QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
508 QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
509 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
510 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
511
512 // When the first source model inserts a new column
513 QCOMPARE(mod.columnCount(), 2);
514 mod.setColumnCount(3);
515
516 // Then the proxy should notify its users and show changes
517 QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
518 QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
519 QCOMPARE(pm.rowCount(), 2);
520 QCOMPARE(pm.columnCount(), 3);
521 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("AB "));
522 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
523
524 // And when removing two columns
525 mod.setColumnCount(1);
526
527 // Then the proxy should notify its users and show changes
528 QCOMPARE(rowSpyToText(colATBRSpy), QStringLiteral("1,2"));
529 QCOMPARE(rowSpyToText(colRemovedSpy), QStringLiteral("1,2"));
530 QCOMPARE(pm.rowCount(), 2);
531 QCOMPARE(pm.columnCount(), 1);
532 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
533 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
534}
535
536void tst_QConcatenateTablesProxyModel::shouldPropagateLayoutChanged()
537{
538 // Given two source models, the second one being a QSFPM
539 QConcatenateTablesProxyModel pm;
540 pm.addSourceModel(sourceModel: &mod);
541 QAbstractItemModelTester modelTest(&pm, this);
542
543 QSortFilterProxyModel qsfpm;
544 qsfpm.setSourceModel(&mod3);
545 pm.addSourceModel(sourceModel: &qsfpm);
546
547 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
548 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
549 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
550
551 // And a selection (row 1)
552 QItemSelectionModel selection(&pm);
553 selection.select(index: pm.index(row: 1, column: 0), command: QItemSelectionModel::Select | QItemSelectionModel::Rows);
554 const QModelIndexList lst = selection.selectedIndexes();
555 QCOMPARE(lst.count(), 3);
556 for (int col = 0; col < lst.count(); ++col) {
557 QCOMPARE(lst.at(col).row(), 1);
558 QCOMPARE(lst.at(col).column(), col);
559 }
560
561 QSignalSpy layoutATBCSpy(&pm, SIGNAL(layoutAboutToBeChanged()));
562 QSignalSpy layoutChangedSpy(&pm, SIGNAL(layoutChanged()));
563
564 // When changing the sorting in the QSFPM
565 qsfpm.sort(column: 0, order: Qt::DescendingOrder);
566
567 // Then the proxy should emit the layoutChanged signals, and show re-sorted data
568 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
569 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
570 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
571 QCOMPARE(layoutATBCSpy.count(), 1);
572 QCOMPARE(layoutChangedSpy.count(), 1);
573
574 // And the selection should be updated accordingly (it became row 2)
575 const QModelIndexList lstAfter = selection.selectedIndexes();
576 QCOMPARE(lstAfter.count(), 3);
577 for (int col = 0; col < lstAfter.count(); ++col) {
578 QCOMPARE(lstAfter.at(col).row(), 2);
579 QCOMPARE(lstAfter.at(col).column(), col);
580 }
581}
582
583void tst_QConcatenateTablesProxyModel::shouldReactToModelReset()
584{
585 // Given two source models, the second one being a QSFPM
586 QConcatenateTablesProxyModel pm;
587 pm.addSourceModel(sourceModel: &mod);
588 QAbstractItemModelTester modelTest(&pm, this);
589
590 QSortFilterProxyModel qsfpm;
591 qsfpm.setSourceModel(&mod3);
592 pm.addSourceModel(sourceModel: &qsfpm);
593
594 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
595 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
596 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
597 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
598 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
599 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
600 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
601 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
602 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
603 QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
604 QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
605
606 // When changing the source model of the QSFPM
607 qsfpm.setSourceModel(&mod2);
608
609 // Then the proxy should emit the reset signals, and show the new data
610 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
611 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
612 QCOMPARE(rowATBRSpy.count(), 0);
613 QCOMPARE(rowRemovedSpy.count(), 0);
614 QCOMPARE(rowATBISpy.count(), 0);
615 QCOMPARE(rowInsertedSpy.count(), 0);
616 QCOMPARE(colATBRSpy.count(), 0);
617 QCOMPARE(colRemovedSpy.count(), 0);
618 QCOMPARE(modelATBResetSpy.count(), 1);
619 QCOMPARE(modelResetSpy.count(), 1);
620}
621
622void tst_QConcatenateTablesProxyModel::shouldUpdateColumnsOnModelReset()
623{
624 // Given two source models, the first one being a QSFPM
625 QConcatenateTablesProxyModel pm;
626
627 QSortFilterProxyModel qsfpm;
628 qsfpm.setSourceModel(&mod3);
629 pm.addSourceModel(sourceModel: &qsfpm);
630 pm.addSourceModel(sourceModel: &mod);
631 QAbstractItemModelTester modelTest(&pm, this);
632
633 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
634 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
635 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("ABC"));
636
637 // ... and a model with only 2 columns
638 QStandardItemModel mod2Columns;
639 mod2Columns.appendRow(items: { new QStandardItem(QStringLiteral("W")), new QStandardItem(QStringLiteral("X")) });
640
641 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
642 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
643 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
644 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
645 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
646 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
647 QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
648 QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
649
650 // When changing the source model of the QSFPM
651 qsfpm.setSourceModel(&mod2Columns);
652
653 // Then the proxy should reset, and show the new data
654 QCOMPARE(modelATBResetSpy.count(), 1);
655 QCOMPARE(modelResetSpy.count(), 1);
656 QCOMPARE(rowATBRSpy.count(), 0);
657 QCOMPARE(rowRemovedSpy.count(), 0);
658 QCOMPARE(rowATBISpy.count(), 0);
659 QCOMPARE(rowInsertedSpy.count(), 0);
660 QCOMPARE(colATBRSpy.count(), 0);
661 QCOMPARE(colRemovedSpy.count(), 0);
662
663 QCOMPARE(pm.rowCount(), 2);
664 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("WX"));
665 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("AB"));
666}
667
668void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem_data()
669{
670 QTest::addColumn<int>(name: "sourceRow");
671 QTest::addColumn<int>(name: "destRow");
672 QTest::addColumn<QString>(name: "expectedResult");
673
674 QTest::newRow(dataTag: "0-3") << 0 << 3 << QStringLiteral("ABCA");
675 QTest::newRow(dataTag: "1-2") << 1 << 2 << QStringLiteral("ABBD");
676 QTest::newRow(dataTag: "2-1") << 2 << 1 << QStringLiteral("ACCD");
677 QTest::newRow(dataTag: "3-0") << 3 << 0 << QStringLiteral("DBCD");
678
679}
680
681void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem()
682{
683 // Given two source models who handle drops
684
685 // Note: QStandardItemModel handles drop onto items by inserting child rows,
686 // which is good for QTreeView but not for QTableView or QConcatenateTablesProxyModel.
687 // So we use QStringListModel here instead.
688 QConcatenateTablesProxyModel pm;
689 QStringListModel model1({QStringLiteral("A"), QStringLiteral("B")});
690 QStringListModel model2({QStringLiteral("C"), QStringLiteral("D")});
691 pm.addSourceModel(sourceModel: &model1);
692 pm.addSourceModel(sourceModel: &model2);
693 QAbstractItemModelTester modelTest(&pm, this);
694 QCOMPARE(extractColumnTexts(&pm, 0), QStringLiteral("ABCD"));
695
696 // When dragging one item
697 QFETCH(int, sourceRow);
698 QMimeData* mimeData = pm.mimeData(indexes: {pm.index(row: sourceRow, column: 0)});
699 QVERIFY(mimeData);
700
701 // and dropping onto another item
702 QFETCH(int, destRow);
703 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
704 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
705 delete mimeData;
706
707 // Then the result should be as expected
708 QFETCH(QString, expectedResult);
709 QCOMPARE(extractColumnTexts(&pm, 0), expectedResult);
710}
711
712void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItems()
713{
714 // Given two models combined
715 QConcatenateTablesProxyModel pm;
716 pm.addSourceModel(sourceModel: &mod3);
717 pm.addSourceModel(sourceModel: &mod2);
718 QAbstractItemModelTester modelTest(&pm, this);
719 QCOMPARE(pm.rowCount(), 3);
720 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
721 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
722 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
723
724 // When dragging the last row
725 QModelIndexList indexes;
726 indexes.reserve(alloc: pm.columnCount());
727 for (int col = 0; col < pm.columnCount(); ++col) {
728 indexes.append(t: pm.index(row: 2, column: col));
729 }
730 QMimeData* mimeData = pm.mimeData(indexes);
731 QVERIFY(mimeData);
732
733 // and dropping it before row 1
734 const int destRow = 1;
735 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
736 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
737 delete mimeData;
738
739 // Then a new row should be inserted
740 QCOMPARE(pm.rowCount(), 4);
741 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
742 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
743 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
744 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
745}
746
747void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItemsAtModelBoundary()
748{
749 // Given two models combined
750 QConcatenateTablesProxyModel pm;
751 pm.addSourceModel(sourceModel: &mod3);
752 pm.addSourceModel(sourceModel: &mod2);
753 QAbstractItemModelTester modelTest(&pm, this);
754 QCOMPARE(pm.rowCount(), 3);
755 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
756 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
757 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
758
759 // When dragging the first row
760 QModelIndexList indexes;
761 indexes.reserve(alloc: pm.columnCount());
762 for (int col = 0; col < pm.columnCount(); ++col) {
763 indexes.append(t: pm.index(row: 0, column: col));
764 }
765 QMimeData* mimeData = pm.mimeData(indexes);
766 QVERIFY(mimeData);
767
768 // and dropping it before row 2
769 const int destRow = 2;
770 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
771 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
772 delete mimeData;
773
774 // Then a new row should be inserted
775 QCOMPARE(pm.rowCount(), 4);
776 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
777 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
778 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
779 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
780
781 // and it should be part of the second model
782 QCOMPARE(mod2.rowCount(), 2);
783}
784
785void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow_data()
786{
787 QTest::addColumn<int>(name: "destRow");
788
789 // Dropping after the last row is documented to be done with destRow == -1.
790 QTest::newRow(dataTag: "-1") << -1;
791 // However, sometimes QTreeView calls dropMimeData with destRow == rowCount...
792 // Not sure if that's a bug or not, but let's support it in the model, just in case.
793 QTest::newRow(dataTag: "3") << 3;
794}
795
796void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow()
797{
798 QFETCH(int, destRow);
799
800 // Given two models combined
801 QConcatenateTablesProxyModel pm;
802 pm.addSourceModel(sourceModel: &mod3);
803 pm.addSourceModel(sourceModel: &mod2);
804 QAbstractItemModelTester modelTest(&pm, this);
805 QCOMPARE(pm.rowCount(), 3);
806 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
807 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
808 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
809
810 // When dragging the second row
811 QModelIndexList indexes;
812 indexes.reserve(alloc: pm.columnCount());
813 for (int col = 0; col < pm.columnCount(); ++col) {
814 indexes.append(t: pm.index(row: 1, column: col));
815 }
816 QMimeData* mimeData = pm.mimeData(indexes);
817 QVERIFY(mimeData);
818
819 // and dropping it after the last row
820 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
821 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
822 delete mimeData;
823
824 // Then a new row should be inserted at the end
825 QCOMPARE(pm.rowCount(), 4);
826 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
827 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
828 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
829 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
830
831}
832
833void tst_QConcatenateTablesProxyModel::qtbug91788()
834{
835 QConcatenateTablesProxyModel proxyConcat;
836 QStringList strList{QString("one"),QString("two")};
837 QStringListModel strListModelA(strList);
838 QSortFilterProxyModel proxyFilter;
839 proxyFilter.setSourceModel(&proxyConcat);
840
841 proxyConcat.addSourceModel(sourceModel: &strListModelA);
842 proxyConcat.removeSourceModel(sourceModel: &strListModelA); // This should not assert
843 QCOMPARE(proxyConcat.columnCount(), 0);
844}
845
846QTEST_GUILESS_MAIN(tst_QConcatenateTablesProxyModel)
847
848#include "tst_qconcatenatetablesproxymodel.moc"
849

source code of qtbase/tests/auto/corelib/itemmodels/qconcatenatetablesproxymodel/tst_qconcatenatetablesproxymodel.cpp