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, 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(1).toString() + QLatin1Char(',') + spy.at(i).at(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
120private:
121 QStandardItemModel mod;
122 QStandardItemModel mod2;
123 QStandardItemModel mod3;
124};
125
126void tst_QConcatenateTablesProxyModel::init()
127{
128 // Prepare some source models to use later on
129 mod.clear();
130 mod.appendRow({ new QStandardItem(QStringLiteral("A")), new QStandardItem(QStringLiteral("B")), new QStandardItem(QStringLiteral("C")) });
131 mod.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
132 mod.setVerticalHeaderLabels(QStringList() << QStringLiteral("One"));
133
134 mod2.clear();
135 mod2.appendRow({ new QStandardItem(QStringLiteral("D")), new QStandardItem(QStringLiteral("E")), new QStandardItem(QStringLiteral("F")) });
136 mod2.setHorizontalHeaderLabels(QStringList() << QStringLiteral("H1") << QStringLiteral("H2") << QStringLiteral("H3"));
137 mod2.setVerticalHeaderLabels(QStringList() << QStringLiteral("Two"));
138
139 mod3.clear();
140 mod3.appendRow({ new QStandardItem(QStringLiteral("1")), new QStandardItem(QStringLiteral("2")), new QStandardItem(QStringLiteral("3")) });
141 mod3.appendRow({ new QStandardItem(QStringLiteral("4")), new QStandardItem(QStringLiteral("5")), new QStandardItem(QStringLiteral("6")) });
142}
143
144void tst_QConcatenateTablesProxyModel::shouldAggregateTwoModelsCorrectly()
145{
146 // Given a combining proxy
147 QConcatenateTablesProxyModel pm;
148
149 // When adding two source models
150 pm.addSourceModel(&mod);
151 pm.addSourceModel(&mod2);
152 QAbstractItemModelTester modelTest(&pm, this);
153
154 // Then the proxy should show 2 rows
155 QCOMPARE(pm.rowCount(), 2);
156 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
157 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
158
159 // ... and correct headers
160 QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
161 QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
162 QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
163 QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
164 QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
165
166 QVERIFY(!pm.canFetchMore(QModelIndex()));
167}
168
169void tst_QConcatenateTablesProxyModel::shouldAggregateThenRemoveTwoEmptyModelsCorrectly()
170{
171 // Given a combining proxy
172 QConcatenateTablesProxyModel pm;
173
174 // When adding two empty models
175 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
176 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
177 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
178 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
179 QIdentityProxyModel i1, i2;
180 pm.addSourceModel(&i1);
181 pm.addSourceModel(&i2);
182
183 // Then the proxy should still be empty (and no signals emitted)
184 QCOMPARE(pm.rowCount(), 0);
185 QCOMPARE(pm.columnCount(), 0);
186 QCOMPARE(rowATBISpy.count(), 0);
187 QCOMPARE(rowInsertedSpy.count(), 0);
188
189 // When removing the empty models
190 pm.removeSourceModel(&i1);
191 pm.removeSourceModel(&i2);
192
193 // Then the proxy should still be empty (and no signals emitted)
194 QCOMPARE(pm.rowCount(), 0);
195 QCOMPARE(pm.columnCount(), 0);
196 QCOMPARE(rowATBRSpy.count(), 0);
197 QCOMPARE(rowRemovedSpy.count(), 0);
198}
199
200void tst_QConcatenateTablesProxyModel::shouldAggregateTwoEmptyModelsWhichThenGetFilled()
201{
202 // Given a combining proxy with two empty models
203 QConcatenateTablesProxyModel pm;
204 QIdentityProxyModel i1, i2;
205 pm.addSourceModel(&i1);
206 pm.addSourceModel(&i2);
207
208 // When filling them afterwards
209 i1.setSourceModel(&mod);
210 i2.setSourceModel(&mod2);
211 QAbstractItemModelTester modelTest(&pm, this);
212
213 // Then the proxy should show 2 rows
214 QCOMPARE(pm.rowCount(), 2);
215 QCOMPARE(pm.columnCount(), 3);
216 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
217 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
218
219 // ... and correct headers
220 QCOMPARE(pm.headerData(0, Qt::Horizontal).toString(), QStringLiteral("H1"));
221 QCOMPARE(pm.headerData(1, Qt::Horizontal).toString(), QStringLiteral("H2"));
222 QCOMPARE(pm.headerData(2, Qt::Horizontal).toString(), QStringLiteral("H3"));
223 QCOMPARE(pm.headerData(0, Qt::Vertical).toString(), QStringLiteral("One"));
224 QCOMPARE(pm.headerData(1, Qt::Vertical).toString(), QStringLiteral("Two"));
225
226 QVERIFY(!pm.canFetchMore(QModelIndex()));
227}
228
229void tst_QConcatenateTablesProxyModel::shouldHandleDataChanged()
230{
231 // Given two models combined
232 QConcatenateTablesProxyModel pm;
233 pm.addSourceModel(&mod);
234 pm.addSourceModel(&mod2);
235 QAbstractItemModelTester modelTest(&pm, this);
236 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
237
238 // When a cell in a source model changes
239 mod.item(0, 0)->setData("a", Qt::EditRole);
240
241 // Then the change should be notified to the proxy
242 QCOMPARE(dataChangedSpy.count(), 1);
243 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
244 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
245
246 // Same test with the other model
247 mod2.item(0, 2)->setData("f", Qt::EditRole);
248
249 QCOMPARE(dataChangedSpy.count(), 2);
250 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
251 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
252}
253
254void tst_QConcatenateTablesProxyModel::shouldHandleSetData()
255{
256 // Given two models combined
257 QConcatenateTablesProxyModel pm;
258 pm.addSourceModel(&mod);
259 pm.addSourceModel(&mod2);
260 QAbstractItemModelTester modelTest(&pm, this);
261 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
262
263 // When changing a cell using setData
264 pm.setData(pm.index(0, 0), "a");
265
266 // Then the change should be notified to the proxy
267 QCOMPARE(dataChangedSpy.count(), 1);
268 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
269 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("aBC"));
270
271 // Same test with the other model
272 pm.setData(pm.index(1, 2), "f");
273
274 QCOMPARE(dataChangedSpy.count(), 2);
275 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
276 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEf"));
277}
278
279void tst_QConcatenateTablesProxyModel::shouldHandleSetItemData()
280{
281 // Given two models combined
282 QConcatenateTablesProxyModel pm;
283 pm.addSourceModel(&mod);
284 pm.addSourceModel(&mod2);
285 QAbstractItemModelTester modelTest(&pm, this);
286 QSignalSpy dataChangedSpy(&pm, SIGNAL(dataChanged(QModelIndex,QModelIndex)));
287
288 // When changing a cell using setData
289 pm.setItemData(pm.index(0, 0), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("X")),
290 std::make_pair<int, QVariant>(Qt::UserRole, 88) });
291
292 // Then the change should be notified to the proxy
293 QCOMPARE(dataChangedSpy.count(), 1);
294 QCOMPARE(dataChangedSpy.at(0).at(0).toModelIndex(), pm.index(0, 0));
295 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("XBC"));
296 QCOMPARE(pm.index(0, 0).data(Qt::UserRole).toInt(), 88);
297
298 // Same test with the other model
299 pm.setItemData(pm.index(1, 2), QMap<int, QVariant>{ std::make_pair<int, QVariant>(Qt::DisplayRole, QStringLiteral("Y")),
300 std::make_pair<int, QVariant>(Qt::UserRole, 89) });
301
302 QCOMPARE(dataChangedSpy.count(), 2);
303 QCOMPARE(dataChangedSpy.at(1).at(0).toModelIndex(), pm.index(1, 2));
304 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEY"));
305 QCOMPARE(pm.index(1, 2).data(Qt::UserRole).toInt(), 89);
306}
307
308void tst_QConcatenateTablesProxyModel::shouldHandleRowInsertionAndRemoval()
309{
310 // Given two models combined
311 QConcatenateTablesProxyModel pm;
312 pm.addSourceModel(&mod);
313 pm.addSourceModel(&mod2);
314 QAbstractItemModelTester modelTest(&pm, this);
315 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
316 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
317
318 // When a source model inserts a new row
319 QList<QStandardItem *> row;
320 row.append(new QStandardItem(QStringLiteral("1")));
321 row.append(new QStandardItem(QStringLiteral("2")));
322 row.append(new QStandardItem(QStringLiteral("3")));
323 mod2.insertRow(0, row);
324
325 // Then the proxy should notify its users and show changes
326 QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("1,1"));
327 QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("1,1"));
328 QCOMPARE(pm.rowCount(), 3);
329 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
330 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
331 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
332
333 // When removing that row
334 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
335 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
336 mod2.removeRow(0);
337
338 // Then the proxy should notify its users and show changes
339 QCOMPARE(rowATBRSpy.count(), 1);
340 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
341 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
342 QCOMPARE(rowRemovedSpy.count(), 1);
343 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
344 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
345 QCOMPARE(pm.rowCount(), 2);
346 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
347 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
348
349 // When removing the last row from mod2
350 rowATBRSpy.clear();
351 rowRemovedSpy.clear();
352 mod2.removeRow(0);
353
354 // Then the proxy should notify its users and show changes
355 QCOMPARE(rowATBRSpy.count(), 1);
356 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
357 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
358 QCOMPARE(rowRemovedSpy.count(), 1);
359 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
360 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
361 QCOMPARE(pm.rowCount(), 1);
362 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
363}
364
365void tst_QConcatenateTablesProxyModel::shouldAggregateAnotherModelThenRemoveModels()
366{
367 // Given two models combined, and a third model
368 QConcatenateTablesProxyModel pm;
369 pm.addSourceModel(&mod);
370 pm.addSourceModel(&mod2);
371 QAbstractItemModelTester modelTest(&pm, this);
372
373 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
374 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
375
376 // When adding the new source model
377 pm.addSourceModel(&mod3);
378
379 // Then the proxy should notify its users about the two rows inserted
380 QCOMPARE(rowSpyToText(rowATBISpy), QStringLiteral("2,3"));
381 QCOMPARE(rowSpyToText(rowInsertedSpy), QStringLiteral("2,3"));
382 QCOMPARE(pm.rowCount(), 4);
383 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
384 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
385 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
386 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
387
388 // When removing that source model again
389 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
390 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
391 pm.removeSourceModel(&mod3);
392
393 // Then the proxy should notify its users about the row removed
394 QCOMPARE(rowATBRSpy.count(), 1);
395 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 2);
396 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 3);
397 QCOMPARE(rowRemovedSpy.count(), 1);
398 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 2);
399 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 3);
400 QCOMPARE(pm.rowCount(), 2);
401 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
402 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
403
404 // When removing model 2
405 rowATBRSpy.clear();
406 rowRemovedSpy.clear();
407 pm.removeSourceModel(&mod2);
408 QCOMPARE(rowATBRSpy.count(), 1);
409 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 1);
410 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 1);
411 QCOMPARE(rowRemovedSpy.count(), 1);
412 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 1);
413 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 1);
414 QCOMPARE(pm.rowCount(), 1);
415 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
416
417 // When removing model 1
418 rowATBRSpy.clear();
419 rowRemovedSpy.clear();
420 pm.removeSourceModel(&mod);
421 QCOMPARE(rowATBRSpy.count(), 1);
422 QCOMPARE(rowATBRSpy.at(0).at(1).toInt(), 0);
423 QCOMPARE(rowATBRSpy.at(0).at(2).toInt(), 0);
424 QCOMPARE(rowRemovedSpy.count(), 1);
425 QCOMPARE(rowRemovedSpy.at(0).at(1).toInt(), 0);
426 QCOMPARE(rowRemovedSpy.at(0).at(2).toInt(), 0);
427 QCOMPARE(pm.rowCount(), 0);
428}
429
430void tst_QConcatenateTablesProxyModel::shouldUseSmallestColumnCount()
431{
432 QConcatenateTablesProxyModel pm;
433 pm.addSourceModel(&mod);
434 pm.addSourceModel(&mod2);
435 mod2.setColumnCount(1);
436 pm.addSourceModel(&mod3);
437 QAbstractItemModelTester modelTest(&pm, this);
438
439 QCOMPARE(pm.rowCount(), 4);
440 QCOMPARE(pm.columnCount(), 1);
441 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
442 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
443 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("1"));
444 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("4"));
445
446 const QModelIndex indexA = pm.mapFromSource(mod.index(0, 0));
447 QVERIFY(indexA.isValid());
448 QCOMPARE(indexA, pm.index(0, 0));
449
450 const QModelIndex indexB = pm.mapFromSource(mod.index(0, 1));
451 QVERIFY(!indexB.isValid());
452
453 const QModelIndex indexD = pm.mapFromSource(mod2.index(0, 0));
454 QVERIFY(indexD.isValid());
455 QCOMPARE(indexD, pm.index(1, 0));
456}
457
458void tst_QConcatenateTablesProxyModel::shouldIncreaseColumnCountWhenRemovingFirstModel()
459{
460 // Given a model with 2 columns and one with 3 columns
461 QConcatenateTablesProxyModel pm;
462 pm.addSourceModel(&mod);
463 QAbstractItemModelTester modelTest(&pm, this);
464 mod.setColumnCount(2);
465 pm.addSourceModel(&mod2);
466 QCOMPARE(pm.rowCount(), 2);
467 QCOMPARE(pm.columnCount(), 2);
468
469 QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
470 QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
471 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
472 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
473
474 // When removing the first source model
475 pm.removeSourceModel(&mod);
476
477 // Then the proxy should notify its users about the row removed, and the column added
478 QCOMPARE(pm.rowCount(), 1);
479 QCOMPARE(pm.columnCount(), 3);
480 QCOMPARE(rowSpyToText(rowATBRSpy), QStringLiteral("0,0"));
481 QCOMPARE(rowSpyToText(rowRemovedSpy), QStringLiteral("0,0"));
482 QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
483 QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
484 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("DEF"));
485}
486
487void tst_QConcatenateTablesProxyModel::shouldHandleColumnInsertionAndRemoval()
488{
489 // Given two models combined, one with 2 columns and one with 3
490 QConcatenateTablesProxyModel pm;
491 pm.addSourceModel(&mod);
492 QAbstractItemModelTester modelTest(&pm, this);
493 mod.setColumnCount(2);
494 pm.addSourceModel(&mod2);
495 QSignalSpy colATBISpy(&pm, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)));
496 QSignalSpy colInsertedSpy(&pm, SIGNAL(columnsInserted(QModelIndex,int,int)));
497 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
498 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
499
500 // When the first source model inserts a new column
501 QCOMPARE(mod.columnCount(), 2);
502 mod.setColumnCount(3);
503
504 // Then the proxy should notify its users and show changes
505 QCOMPARE(rowSpyToText(colATBISpy), QStringLiteral("2,2"));
506 QCOMPARE(rowSpyToText(colInsertedSpy), QStringLiteral("2,2"));
507 QCOMPARE(pm.rowCount(), 2);
508 QCOMPARE(pm.columnCount(), 3);
509 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("AB "));
510 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
511
512 // And when removing two columns
513 mod.setColumnCount(1);
514
515 // Then the proxy should notify its users and show changes
516 QCOMPARE(rowSpyToText(colATBRSpy), QStringLiteral("1,2"));
517 QCOMPARE(rowSpyToText(colRemovedSpy), QStringLiteral("1,2"));
518 QCOMPARE(pm.rowCount(), 2);
519 QCOMPARE(pm.columnCount(), 1);
520 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("A"));
521 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("D"));
522}
523
524void tst_QConcatenateTablesProxyModel::shouldPropagateLayoutChanged()
525{
526 // Given two source models, the second one being a QSFPM
527 QConcatenateTablesProxyModel pm;
528 pm.addSourceModel(&mod);
529 QAbstractItemModelTester modelTest(&pm, this);
530
531 QSortFilterProxyModel qsfpm;
532 qsfpm.setSourceModel(&mod3);
533 pm.addSourceModel(&qsfpm);
534
535 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
536 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
537 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
538
539 // And a selection (row 1)
540 QItemSelectionModel selection(&pm);
541 selection.select(pm.index(1, 0), QItemSelectionModel::Select | QItemSelectionModel::Rows);
542 const QModelIndexList lst = selection.selectedIndexes();
543 QCOMPARE(lst.count(), 3);
544 for (int col = 0; col < lst.count(); ++col) {
545 QCOMPARE(lst.at(col).row(), 1);
546 QCOMPARE(lst.at(col).column(), col);
547 }
548
549 QSignalSpy layoutATBCSpy(&pm, SIGNAL(layoutAboutToBeChanged()));
550 QSignalSpy layoutChangedSpy(&pm, SIGNAL(layoutChanged()));
551
552 // When changing the sorting in the QSFPM
553 qsfpm.sort(0, Qt::DescendingOrder);
554
555 // Then the proxy should emit the layoutChanged signals, and show re-sorted data
556 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
557 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
558 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
559 QCOMPARE(layoutATBCSpy.count(), 1);
560 QCOMPARE(layoutChangedSpy.count(), 1);
561
562 // And the selection should be updated accordingly (it became row 2)
563 const QModelIndexList lstAfter = selection.selectedIndexes();
564 QCOMPARE(lstAfter.count(), 3);
565 for (int col = 0; col < lstAfter.count(); ++col) {
566 QCOMPARE(lstAfter.at(col).row(), 2);
567 QCOMPARE(lstAfter.at(col).column(), col);
568 }
569}
570
571void tst_QConcatenateTablesProxyModel::shouldReactToModelReset()
572{
573 // Given two source models, the second one being a QSFPM
574 QConcatenateTablesProxyModel pm;
575 pm.addSourceModel(&mod);
576 QAbstractItemModelTester modelTest(&pm, this);
577
578 QSortFilterProxyModel qsfpm;
579 qsfpm.setSourceModel(&mod3);
580 pm.addSourceModel(&qsfpm);
581
582 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
583 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("123"));
584 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
585 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
586 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
587 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
588 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
589 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
590 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
591 QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
592 QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
593
594 // When changing the source model of the QSFPM
595 qsfpm.setSourceModel(&mod2);
596
597 // Then the proxy should emit the reset signals, and show the new data
598 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("ABC"));
599 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
600 QCOMPARE(rowATBRSpy.count(), 0);
601 QCOMPARE(rowRemovedSpy.count(), 0);
602 QCOMPARE(rowATBISpy.count(), 0);
603 QCOMPARE(rowInsertedSpy.count(), 0);
604 QCOMPARE(colATBRSpy.count(), 0);
605 QCOMPARE(colRemovedSpy.count(), 0);
606 QCOMPARE(modelATBResetSpy.count(), 1);
607 QCOMPARE(modelResetSpy.count(), 1);
608}
609
610void tst_QConcatenateTablesProxyModel::shouldUpdateColumnsOnModelReset()
611{
612 // Given two source models, the first one being a QSFPM
613 QConcatenateTablesProxyModel pm;
614
615 QSortFilterProxyModel qsfpm;
616 qsfpm.setSourceModel(&mod3);
617 pm.addSourceModel(&qsfpm);
618 pm.addSourceModel(&mod);
619 QAbstractItemModelTester modelTest(&pm, this);
620
621 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
622 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
623 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("ABC"));
624
625 // ... and a model with only 2 columns
626 QStandardItemModel mod2Columns;
627 mod2Columns.appendRow({ new QStandardItem(QStringLiteral("W")), new QStandardItem(QStringLiteral("X")) });
628
629 QSignalSpy rowATBRSpy(&pm, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)));
630 QSignalSpy rowRemovedSpy(&pm, SIGNAL(rowsRemoved(QModelIndex,int,int)));
631 QSignalSpy rowATBISpy(&pm, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)));
632 QSignalSpy rowInsertedSpy(&pm, SIGNAL(rowsInserted(QModelIndex,int,int)));
633 QSignalSpy colATBRSpy(&pm, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)));
634 QSignalSpy colRemovedSpy(&pm, SIGNAL(columnsRemoved(QModelIndex,int,int)));
635 QSignalSpy modelATBResetSpy(&pm, SIGNAL(modelAboutToBeReset()));
636 QSignalSpy modelResetSpy(&pm, SIGNAL(modelReset()));
637
638 // When changing the source model of the QSFPM
639 qsfpm.setSourceModel(&mod2Columns);
640
641 // Then the proxy should reset, and show the new data
642 QCOMPARE(modelATBResetSpy.count(), 1);
643 QCOMPARE(modelResetSpy.count(), 1);
644 QCOMPARE(rowATBRSpy.count(), 0);
645 QCOMPARE(rowRemovedSpy.count(), 0);
646 QCOMPARE(rowATBISpy.count(), 0);
647 QCOMPARE(rowInsertedSpy.count(), 0);
648 QCOMPARE(colATBRSpy.count(), 0);
649 QCOMPARE(colRemovedSpy.count(), 0);
650
651 QCOMPARE(pm.rowCount(), 2);
652 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("WX"));
653 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("AB"));
654}
655
656void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem_data()
657{
658 QTest::addColumn<int>("sourceRow");
659 QTest::addColumn<int>("destRow");
660 QTest::addColumn<QString>("expectedResult");
661
662 QTest::newRow("0-3") << 0 << 3 << QStringLiteral("ABCA");
663 QTest::newRow("1-2") << 1 << 2 << QStringLiteral("ABBD");
664 QTest::newRow("2-1") << 2 << 1 << QStringLiteral("ACCD");
665 QTest::newRow("3-0") << 3 << 0 << QStringLiteral("DBCD");
666
667}
668
669void tst_QConcatenateTablesProxyModel::shouldPropagateDropOnItem()
670{
671 // Given two source models who handle drops
672
673 // Note: QStandardItemModel handles drop onto items by inserting child rows,
674 // which is good for QTreeView but not for QTableView or QConcatenateTablesProxyModel.
675 // So we use QStringListModel here instead.
676 QConcatenateTablesProxyModel pm;
677 QStringListModel model1({QStringLiteral("A"), QStringLiteral("B")});
678 QStringListModel model2({QStringLiteral("C"), QStringLiteral("D")});
679 pm.addSourceModel(&model1);
680 pm.addSourceModel(&model2);
681 QAbstractItemModelTester modelTest(&pm, this);
682 QCOMPARE(extractColumnTexts(&pm, 0), QStringLiteral("ABCD"));
683
684 // When dragging one item
685 QFETCH(int, sourceRow);
686 QMimeData* mimeData = pm.mimeData({pm.index(sourceRow, 0)});
687 QVERIFY(mimeData);
688
689 // and dropping onto another item
690 QFETCH(int, destRow);
691 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
692 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, -1, -1, pm.index(destRow, 0)));
693 delete mimeData;
694
695 // Then the result should be as expected
696 QFETCH(QString, expectedResult);
697 QCOMPARE(extractColumnTexts(&pm, 0), expectedResult);
698}
699
700void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItems()
701{
702 // Given two models combined
703 QConcatenateTablesProxyModel pm;
704 pm.addSourceModel(&mod3);
705 pm.addSourceModel(&mod2);
706 QAbstractItemModelTester modelTest(&pm, this);
707 QCOMPARE(pm.rowCount(), 3);
708 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
709 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
710 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
711
712 // When dragging the last row
713 QModelIndexList indexes;
714 indexes.reserve(pm.columnCount());
715 for (int col = 0; col < pm.columnCount(); ++col) {
716 indexes.append(pm.index(2, col));
717 }
718 QMimeData* mimeData = pm.mimeData(indexes);
719 QVERIFY(mimeData);
720
721 // and dropping it before row 1
722 const int destRow = 1;
723 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
724 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
725 delete mimeData;
726
727 // Then a new row should be inserted
728 QCOMPARE(pm.rowCount(), 4);
729 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
730 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("DEF"));
731 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("456"));
732 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
733}
734
735void tst_QConcatenateTablesProxyModel::shouldPropagateDropBetweenItemsAtModelBoundary()
736{
737 // Given two models combined
738 QConcatenateTablesProxyModel pm;
739 pm.addSourceModel(&mod3);
740 pm.addSourceModel(&mod2);
741 QAbstractItemModelTester modelTest(&pm, this);
742 QCOMPARE(pm.rowCount(), 3);
743 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
744 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
745 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
746
747 // When dragging the first row
748 QModelIndexList indexes;
749 indexes.reserve(pm.columnCount());
750 for (int col = 0; col < pm.columnCount(); ++col) {
751 indexes.append(pm.index(0, col));
752 }
753 QMimeData* mimeData = pm.mimeData(indexes);
754 QVERIFY(mimeData);
755
756 // and dropping it before row 2
757 const int destRow = 2;
758 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
759 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
760 delete mimeData;
761
762 // Then a new row should be inserted
763 QCOMPARE(pm.rowCount(), 4);
764 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
765 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
766 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("123"));
767 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("DEF"));
768
769 // and it should be part of the second model
770 QCOMPARE(mod2.rowCount(), 2);
771}
772
773void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow_data()
774{
775 QTest::addColumn<int>("destRow");
776
777 // Dropping after the last row is documented to be done with destRow == -1.
778 QTest::newRow("-1") << -1;
779 // However, sometimes QTreeView calls dropMimeData with destRow == rowCount...
780 // Not sure if that's a bug or not, but let's support it in the model, just in case.
781 QTest::newRow("3") << 3;
782}
783
784void tst_QConcatenateTablesProxyModel::shouldPropagateDropAfterLastRow()
785{
786 QFETCH(int, destRow);
787
788 // Given two models combined
789 QConcatenateTablesProxyModel pm;
790 pm.addSourceModel(&mod3);
791 pm.addSourceModel(&mod2);
792 QAbstractItemModelTester modelTest(&pm, this);
793 QCOMPARE(pm.rowCount(), 3);
794 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
795 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
796 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
797
798 // When dragging the second row
799 QModelIndexList indexes;
800 indexes.reserve(pm.columnCount());
801 for (int col = 0; col < pm.columnCount(); ++col) {
802 indexes.append(pm.index(1, col));
803 }
804 QMimeData* mimeData = pm.mimeData(indexes);
805 QVERIFY(mimeData);
806
807 // and dropping it after the last row
808 QVERIFY(pm.canDropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
809 QVERIFY(pm.dropMimeData(mimeData, Qt::CopyAction, destRow, 0, QModelIndex()));
810 delete mimeData;
811
812 // Then a new row should be inserted at the end
813 QCOMPARE(pm.rowCount(), 4);
814 QCOMPARE(extractRowTexts(&pm, 0), QStringLiteral("123"));
815 QCOMPARE(extractRowTexts(&pm, 1), QStringLiteral("456"));
816 QCOMPARE(extractRowTexts(&pm, 2), QStringLiteral("DEF"));
817 QCOMPARE(extractRowTexts(&pm, 3), QStringLiteral("456"));
818
819}
820
821QTEST_GUILESS_MAIN(tst_QConcatenateTablesProxyModel)
822
823#include "tst_qconcatenatetablesproxymodel.moc"
824