1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2018 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, authors Filipe Azevedo <filipe.azevedo@kdab.com> and David Faure <david.faure@kdab.com> |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the test suite of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
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 General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
21 | ** included in the packaging of this file. Please review the following |
22 | ** information to ensure the GNU General Public License requirements will |
23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
24 | ** |
25 | ** $QT_END_LICENSE$ |
26 | ** |
27 | ****************************************************************************/ |
28 | |
29 | #include <QTest> |
30 | #include <QSignalSpy> |
31 | #include <QSortFilterProxyModel> |
32 | #include <QStandardItem> |
33 | |
34 | Q_DECLARE_METATYPE(QModelIndex) |
35 | |
36 | static const int s_filterRole = Qt::UserRole + 1; |
37 | |
38 | class ModelSignalSpy : public QObject { |
39 | Q_OBJECT |
40 | public: |
41 | explicit ModelSignalSpy(QAbstractItemModel &model) { |
42 | connect(sender: &model, signal: &QAbstractItemModel::rowsInserted, receiver: this, slot: &ModelSignalSpy::onRowsInserted); |
43 | connect(sender: &model, signal: &QAbstractItemModel::rowsRemoved, receiver: this, slot: &ModelSignalSpy::onRowsRemoved); |
44 | connect(sender: &model, signal: &QAbstractItemModel::rowsAboutToBeInserted, receiver: this, slot: &ModelSignalSpy::onRowsAboutToBeInserted); |
45 | connect(sender: &model, signal: &QAbstractItemModel::rowsAboutToBeRemoved, receiver: this, slot: &ModelSignalSpy::onRowsAboutToBeRemoved); |
46 | connect(sender: &model, signal: &QAbstractItemModel::rowsMoved, receiver: this, slot: &ModelSignalSpy::onRowsMoved); |
47 | connect(sender: &model, signal: &QAbstractItemModel::dataChanged, receiver: this, slot: &ModelSignalSpy::onDataChanged); |
48 | connect(sender: &model, signal: &QAbstractItemModel::layoutChanged, receiver: this, slot: &ModelSignalSpy::onLayoutChanged); |
49 | connect(sender: &model, signal: &QAbstractItemModel::modelReset, receiver: this, slot: &ModelSignalSpy::onModelReset); |
50 | } |
51 | |
52 | QStringList mSignals; |
53 | |
54 | private Q_SLOTS: |
55 | void onRowsInserted(QModelIndex p, int start, int end) { |
56 | mSignals << QLatin1String("rowsInserted(" ) + textForRowSpy(parent: p, start, end) + ')'; |
57 | } |
58 | void onRowsRemoved(QModelIndex p, int start, int end) { |
59 | mSignals << QLatin1String("rowsRemoved(" ) + textForRowSpy(parent: p, start, end) + ')'; |
60 | } |
61 | void onRowsAboutToBeInserted(QModelIndex p, int start, int end) { |
62 | mSignals << QLatin1String("rowsAboutToBeInserted(" ) + textForRowSpy(parent: p, start, end) + ')'; |
63 | } |
64 | void onRowsAboutToBeRemoved(QModelIndex p, int start, int end) { |
65 | mSignals << QLatin1String("rowsAboutToBeRemoved(" ) + textForRowSpy(parent: p, start, end) + ')'; |
66 | } |
67 | void onRowsMoved(QModelIndex,int,int,QModelIndex,int) { |
68 | mSignals << QStringLiteral("rowsMoved" ); |
69 | } |
70 | void onDataChanged(const QModelIndex &from, const QModelIndex& ) { |
71 | mSignals << QStringLiteral("dataChanged(%1)" ).arg(a: from.data(arole: Qt::DisplayRole).toString()); |
72 | } |
73 | void onLayoutChanged() { |
74 | mSignals << QStringLiteral("layoutChanged" ); |
75 | } |
76 | void onModelReset() { |
77 | mSignals << QStringLiteral("modelReset" ); |
78 | } |
79 | private: |
80 | QString textForRowSpy(const QModelIndex &parent, int start, int end) |
81 | { |
82 | QString txt = parent.data(arole: Qt::DisplayRole).toString(); |
83 | if (!txt.isEmpty()) |
84 | txt += QLatin1Char('.'); |
85 | txt += QString::number(start+1); |
86 | if (start != end) |
87 | txt += QLatin1Char('-') + QString::number(end+1); |
88 | return txt; |
89 | } |
90 | }; |
91 | |
92 | class TestModel : public QSortFilterProxyModel |
93 | { |
94 | Q_OBJECT |
95 | public: |
96 | TestModel(QAbstractItemModel *sourceModel) |
97 | : QSortFilterProxyModel() |
98 | { |
99 | setFilterRole(s_filterRole); |
100 | setRecursiveFilteringEnabled(true); |
101 | setSourceModel(sourceModel); |
102 | } |
103 | |
104 | virtual bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override |
105 | { |
106 | return sourceModel()->index(row: sourceRow, column: 0, parent: sourceParent).data(arole: s_filterRole).toBool() |
107 | && QSortFilterProxyModel::filterAcceptsRow(source_row: sourceRow, source_parent: sourceParent); |
108 | } |
109 | }; |
110 | |
111 | // Represents this tree |
112 | // - A |
113 | // - - B |
114 | // - - - C |
115 | // - - - D |
116 | // - - E |
117 | // as a single string, englobing children in brackets, like this: |
118 | // [A[B[C D] E]] |
119 | // In addition, items that match the filtering (data(s_filterRole) == true) have a * after their value. |
120 | static QString treeAsString(const QAbstractItemModel &model, const QModelIndex &parent = QModelIndex()) |
121 | { |
122 | QString ret; |
123 | const int rowCount = model.rowCount(parent); |
124 | if (rowCount > 0) { |
125 | ret += QLatin1Char('['); |
126 | for (int row = 0 ; row < rowCount; ++row) { |
127 | if (row > 0) { |
128 | ret += ' '; |
129 | } |
130 | const QModelIndex child = model.index(row, column: 0, parent); |
131 | ret += child.data(arole: Qt::DisplayRole).toString(); |
132 | if (child.data(arole: s_filterRole).toBool()) |
133 | ret += QLatin1Char('*'); |
134 | ret += treeAsString(model, parent: child); |
135 | } |
136 | ret += QLatin1Char(']'); |
137 | } |
138 | return ret; |
139 | } |
140 | |
141 | // Fill a tree model based on a string representation (see treeAsString) |
142 | static void fillModel(QStandardItemModel &model, const QString &str) |
143 | { |
144 | QCOMPARE(str.count('['), str.count(']')); |
145 | QStandardItem *item = 0; |
146 | QString data; |
147 | for ( int i = 0 ; i < str.length() ; ++i ) { |
148 | const QChar ch = str.at(i); |
149 | if ((ch == '[' || ch == ']' || ch == ' ') && !data.isEmpty()) { |
150 | if (data.endsWith(c: '*')) { |
151 | item->setData(value: true, role: s_filterRole); |
152 | data.chop(n: 1); |
153 | } |
154 | item->setText(data); |
155 | data.clear(); |
156 | } |
157 | if (ch == '[') { |
158 | // Create new child |
159 | QStandardItem *child = new QStandardItem; |
160 | if (item) |
161 | item->appendRow(aitem: child); |
162 | else |
163 | model.appendRow(aitem: child); |
164 | item = child; |
165 | } else if (ch == ']') { |
166 | // Go up to parent |
167 | item = item->parent(); |
168 | } else if (ch == ' ') { |
169 | // Create new sibling |
170 | QStandardItem *child = new QStandardItem; |
171 | QStandardItem *parent = item->parent(); |
172 | if (parent) |
173 | parent->appendRow(aitem: child); |
174 | else |
175 | model.appendRow(aitem: child); |
176 | item = child; |
177 | } else { |
178 | data += ch; |
179 | } |
180 | } |
181 | } |
182 | |
183 | class tst_QSortFilterProxyModel_Recursive : public QObject |
184 | { |
185 | Q_OBJECT |
186 | private: |
187 | private Q_SLOTS: |
188 | void testInitialFiltering_data() |
189 | { |
190 | QTest::addColumn<QString>(name: "sourceStr" ); |
191 | QTest::addColumn<QString>(name: "proxyStr" ); |
192 | |
193 | QTest::newRow(dataTag: "empty" ) << "[]" << "" ; |
194 | QTest::newRow(dataTag: "no" ) << "[1]" << "" ; |
195 | QTest::newRow(dataTag: "yes" ) << "[1*]" << "[1*]" ; |
196 | QTest::newRow(dataTag: "second" ) << "[1 2*]" << "[2*]" ; |
197 | QTest::newRow(dataTag: "child_yes" ) << "[1 2[2.1*]]" << "[2[2.1*]]" ; |
198 | QTest::newRow(dataTag: "grandchild_yes" ) << "[1 2[2.1[2.1.1*]]]" << "[2[2.1[2.1.1*]]]" ; |
199 | // 1, 3.1 and 4.2.1 match, so their parents are in the model |
200 | QTest::newRow(dataTag: "more" ) << "[1* 2[2.1] 3[3.1*] 4[4.1 4.2[4.2.1*]]]" << "[1* 3[3.1*] 4[4.2[4.2.1*]]]" ; |
201 | } |
202 | |
203 | void testInitialFiltering() |
204 | { |
205 | QFETCH(QString, sourceStr); |
206 | QFETCH(QString, proxyStr); |
207 | |
208 | QStandardItemModel model; |
209 | fillModel(model, str: sourceStr); |
210 | QCOMPARE(treeAsString(model), sourceStr); |
211 | |
212 | TestModel proxy(&model); |
213 | QVERIFY(proxy.isRecursiveFilteringEnabled()); |
214 | QCOMPARE(treeAsString(proxy), proxyStr); |
215 | } |
216 | |
217 | // Test changing a role that is unrelated to the filtering. |
218 | void testUnrelatedDataChange() |
219 | { |
220 | QStandardItemModel model; |
221 | const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]" ); |
222 | fillModel(model, str: sourceStr); |
223 | QCOMPARE(treeAsString(model), sourceStr); |
224 | |
225 | TestModel proxy(&model); |
226 | QCOMPARE(treeAsString(proxy), sourceStr); |
227 | |
228 | ModelSignalSpy spy(proxy); |
229 | QStandardItem *item_1_1_1 = model.item(row: 0)->child(row: 0)->child(row: 0); |
230 | |
231 | // When changing the text on the item |
232 | item_1_1_1->setText(QStringLiteral("ME" )); |
233 | |
234 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[ME*]]]" )); |
235 | |
236 | // filterRole is Qt::UserRole + 1, so parents are not checked and |
237 | // therefore no dataChanged for parents |
238 | QCOMPARE(spy.mSignals, QStringList() |
239 | << QStringLiteral("dataChanged(ME)" )); |
240 | } |
241 | |
242 | // Test changing a role that is unrelated to the filtering, in a hidden item. |
243 | void testHiddenDataChange() |
244 | { |
245 | QStandardItemModel model; |
246 | const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]" ); |
247 | fillModel(model, str: sourceStr); |
248 | QCOMPARE(treeAsString(model), sourceStr); |
249 | |
250 | TestModel proxy(&model); |
251 | QCOMPARE(treeAsString(proxy), QString()); |
252 | |
253 | ModelSignalSpy spy(proxy); |
254 | QStandardItem *item_1_1_1 = model.item(row: 0)->child(row: 0)->child(row: 0); |
255 | |
256 | // When changing the text on a hidden item |
257 | item_1_1_1->setText(QStringLiteral("ME" )); |
258 | |
259 | QCOMPARE(treeAsString(proxy), QString()); |
260 | QCOMPARE(spy.mSignals, QStringList()); |
261 | } |
262 | |
263 | // Test that we properly react to a data-changed signal in a descendant and include all required rows |
264 | void testDataChangeIn_data() |
265 | { |
266 | QTest::addColumn<QString>(name: "sourceStr" ); |
267 | QTest::addColumn<QString>(name: "initialProxyStr" ); |
268 | QTest::addColumn<QString>(name: "add" ); // set the flag on this item |
269 | QTest::addColumn<QString>(name: "expectedProxyStr" ); |
270 | QTest::addColumn<QStringList>(name: "expectedSignals" ); |
271 | |
272 | QTest::newRow(dataTag: "toplevel" ) << "[1]" << "" << "1" << "[1*]" |
273 | << (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)" ) << QStringLiteral("rowsInserted(1)" )); |
274 | QTest::newRow(dataTag: "show_parents" ) << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "[1[1.1[1.1.1*]]]" |
275 | << (QStringList() << QStringLiteral("rowsAboutToBeInserted(1)" ) << QStringLiteral("rowsInserted(1)" )); |
276 | |
277 | const QStringList insert_1_1_1 = QStringList() |
278 | << QStringLiteral("rowsAboutToBeInserted(1.1.1)" ) |
279 | << QStringLiteral("rowsInserted(1.1.1)" ) |
280 | << QStringLiteral("dataChanged(1.1)" ) |
281 | << QStringLiteral("dataChanged(1)" ) |
282 | ; |
283 | QTest::newRow(dataTag: "parent_visible" ) << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*[1.1.1*]]]" |
284 | << insert_1_1_1; |
285 | |
286 | QTest::newRow(dataTag: "sibling_visible" ) << "[1[1.1[1.1.1 1.1.2*]]]" << "[1[1.1[1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2*]]]" |
287 | << insert_1_1_1; |
288 | |
289 | QTest::newRow(dataTag: "visible_cousin" ) << "[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" |
290 | << insert_1_1_1; |
291 | |
292 | QTest::newRow(dataTag: "show_parent" ) << "[1[1.1[1.1.1 1.1.2] 1.2*]]" << "[1[1.2*]]" << "1.1.1" << "[1[1.1[1.1.1*] 1.2*]]" |
293 | << (QStringList() |
294 | << QStringLiteral("rowsAboutToBeInserted(1.1)" ) |
295 | << QStringLiteral("rowsInserted(1.1)" ) |
296 | << QStringLiteral("dataChanged(1)" )); |
297 | |
298 | QTest::newRow(dataTag: "with_children" ) << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" |
299 | << (QStringList() |
300 | << QStringLiteral("dataChanged(1.1.1)" ) |
301 | << QStringLiteral("dataChanged(1.1)" ) |
302 | << QStringLiteral("dataChanged(1)" )); |
303 | |
304 | } |
305 | |
306 | void testDataChangeIn() |
307 | { |
308 | QFETCH(QString, sourceStr); |
309 | QFETCH(QString, initialProxyStr); |
310 | QFETCH(QString, add); |
311 | QFETCH(QString, expectedProxyStr); |
312 | QFETCH(QStringList, expectedSignals); |
313 | |
314 | QStandardItemModel model; |
315 | fillModel(model, str: sourceStr); |
316 | QCOMPARE(treeAsString(model), sourceStr); |
317 | |
318 | TestModel proxy(&model); |
319 | QCOMPARE(treeAsString(proxy), initialProxyStr); |
320 | |
321 | ModelSignalSpy spy(proxy); |
322 | // When changing the data on the designated item to show this row |
323 | QStandardItem *itemToChange = itemByText(model, text: add); |
324 | QVERIFY(!itemToChange->data(s_filterRole).toBool()); |
325 | itemToChange->setData(value: true, role: s_filterRole); |
326 | |
327 | // The proxy should update as expected |
328 | QCOMPARE(treeAsString(proxy), expectedProxyStr); |
329 | |
330 | //qDebug() << spy.mSignals; |
331 | QCOMPARE(spy.mSignals, expectedSignals); |
332 | } |
333 | |
334 | void testDataChangeOut_data() |
335 | { |
336 | QTest::addColumn<QString>(name: "sourceStr" ); |
337 | QTest::addColumn<QString>(name: "initialProxyStr" ); |
338 | QTest::addColumn<QString>(name: "remove" ); // unset the flag on this item |
339 | QTest::addColumn<QString>(name: "expectedProxyStr" ); |
340 | QTest::addColumn<QStringList>(name: "expectedSignals" ); |
341 | |
342 | const QStringList remove1_1_1 = (QStringList() |
343 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
344 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
345 | << QStringLiteral("dataChanged(1.1)" ) |
346 | << QStringLiteral("dataChanged(1)" )); |
347 | |
348 | QTest::newRow(dataTag: "toplevel" ) << "[1*]" << "[1*]" << "1" << "" |
349 | << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)" ) << QStringLiteral("rowsRemoved(1)" )); |
350 | |
351 | QTest::newRow(dataTag: "hide_parent" ) << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << "" << |
352 | (QStringList() |
353 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
354 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
355 | << QStringLiteral("rowsAboutToBeRemoved(1.1)" ) |
356 | << QStringLiteral("rowsRemoved(1.1)" ) |
357 | << QStringLiteral("rowsAboutToBeRemoved(1)" ) |
358 | << QStringLiteral("rowsRemoved(1)" )); |
359 | |
360 | QTest::newRow(dataTag: "parent_visible" ) << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]" |
361 | << remove1_1_1; |
362 | |
363 | QTest::newRow(dataTag: "visible" ) << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]" |
364 | << remove1_1_1; |
365 | QTest::newRow(dataTag: "visible_cousin" ) << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]" |
366 | << remove1_1_1; |
367 | |
368 | // The following tests trigger the removal of an ascendant. |
369 | QTest::newRow(dataTag: "remove_parent" ) << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]" |
370 | << (QStringList() |
371 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
372 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
373 | << QStringLiteral("rowsAboutToBeRemoved(1.1)" ) |
374 | << QStringLiteral("rowsRemoved(1.1)" ) |
375 | << QStringLiteral("dataChanged(1)" )); |
376 | |
377 | QTest::newRow(dataTag: "with_children" ) << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1*[1.1.1.1*]]] 2*]" << "1.1.1" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" |
378 | << (QStringList() |
379 | << QStringLiteral("dataChanged(1.1.1)" ) |
380 | << QStringLiteral("dataChanged(1.1)" ) |
381 | << QStringLiteral("dataChanged(1)" )); |
382 | |
383 | QTest::newRow(dataTag: "last_visible" ) << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << "" |
384 | << (QStringList() |
385 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
386 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
387 | << QStringLiteral("rowsAboutToBeRemoved(1.1)" ) |
388 | << QStringLiteral("rowsRemoved(1.1)" ) |
389 | << QStringLiteral("rowsAboutToBeRemoved(1)" ) |
390 | << QStringLiteral("rowsRemoved(1)" )); |
391 | |
392 | } |
393 | |
394 | void testDataChangeOut() |
395 | { |
396 | QFETCH(QString, sourceStr); |
397 | QFETCH(QString, initialProxyStr); |
398 | QFETCH(QString, remove); |
399 | QFETCH(QString, expectedProxyStr); |
400 | QFETCH(QStringList, expectedSignals); |
401 | |
402 | QStandardItemModel model; |
403 | fillModel(model, str: sourceStr); |
404 | QCOMPARE(treeAsString(model), sourceStr); |
405 | |
406 | TestModel proxy(&model); |
407 | QCOMPARE(treeAsString(proxy), initialProxyStr); |
408 | |
409 | ModelSignalSpy spy(proxy); |
410 | |
411 | // When changing the data on the designated item to exclude this row again |
412 | QStandardItem *itemToChange = itemByText(model, text: remove); |
413 | QVERIFY(itemToChange->data(s_filterRole).toBool()); |
414 | itemToChange->setData(value: false, role: s_filterRole); |
415 | |
416 | // The proxy should update as expected |
417 | QCOMPARE(treeAsString(proxy), expectedProxyStr); |
418 | |
419 | //qDebug() << spy.mSignals; |
420 | QCOMPARE(spy.mSignals, expectedSignals); |
421 | } |
422 | |
423 | void testInsert() |
424 | { |
425 | QStandardItemModel model; |
426 | const QString sourceStr = QStringLiteral("[1[1.1[1.1.1]]]" ); |
427 | fillModel(model, str: sourceStr); |
428 | QCOMPARE(treeAsString(model), sourceStr); |
429 | |
430 | TestModel proxy(&model); |
431 | QCOMPARE(treeAsString(proxy), QString()); |
432 | |
433 | ModelSignalSpy spy(proxy); |
434 | QStandardItem *item_1_1_1 = model.item(row: 0)->child(row: 0)->child(row: 0); |
435 | QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1" )); |
436 | item_1_1_1_1->setData(value: true, role: s_filterRole); |
437 | item_1_1_1->appendRow(aitem: item_1_1_1_1); |
438 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]" )); |
439 | |
440 | QCOMPARE(spy.mSignals, QStringList() << QStringLiteral("rowsAboutToBeInserted(1)" ) |
441 | << QStringLiteral("rowsInserted(1)" )); |
442 | } |
443 | |
444 | // Start from [1[1.1[1.1.1 1.1.2[1.1.2.1*]]]] |
445 | // where 1.1.1 is hidden but 1.1 is shown, we want to insert a shown child in 1.1.1. |
446 | // The proxy ensures dataChanged is called on 1.1, |
447 | // so that 1.1.1 and 1.1.1.1 are included in the model. |
448 | void testInsertCousin() |
449 | { |
450 | QStandardItemModel model; |
451 | const QString sourceStr = QStringLiteral("[1[1.1[1.1.1 1.1.2[1.1.2.1*]]]]" ); |
452 | fillModel(model, str: sourceStr); |
453 | QCOMPARE(treeAsString(model), sourceStr); |
454 | |
455 | TestModel proxy(&model); |
456 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.2[1.1.2.1*]]]]" )); |
457 | |
458 | ModelSignalSpy spy(proxy); |
459 | { |
460 | QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1" )); |
461 | item_1_1_1_1->setData(value: true, role: s_filterRole); |
462 | QStandardItem *item_1_1_1 = model.item(row: 0)->child(row: 0)->child(row: 0); |
463 | item_1_1_1->appendRow(aitem: item_1_1_1_1); |
464 | } |
465 | |
466 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*] 1.1.2[1.1.2.1*]]]]" )); |
467 | //qDebug() << spy.mSignals; |
468 | QCOMPARE(spy.mSignals, QStringList() |
469 | << QStringLiteral("rowsAboutToBeInserted(1.1.1)" ) |
470 | << QStringLiteral("rowsInserted(1.1.1)" ) |
471 | << QStringLiteral("dataChanged(1.1)" ) |
472 | << QStringLiteral("dataChanged(1)" )); |
473 | } |
474 | |
475 | void testInsertWithChildren() |
476 | { |
477 | QStandardItemModel model; |
478 | const QString sourceStr = QStringLiteral("[1[1.1]]" ); |
479 | fillModel(model, str: sourceStr); |
480 | QCOMPARE(treeAsString(model), sourceStr); |
481 | |
482 | TestModel proxy(&model); |
483 | QCOMPARE(treeAsString(proxy), QString()); |
484 | |
485 | ModelSignalSpy spy(proxy); |
486 | { |
487 | QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1" )); |
488 | QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1" )); |
489 | item_1_1_1_1->setData(value: true, role: s_filterRole); |
490 | item_1_1_1->appendRow(aitem: item_1_1_1_1); |
491 | |
492 | QStandardItem *item_1_1 = model.item(row: 0)->child(row: 0); |
493 | item_1_1->appendRow(aitem: item_1_1_1); |
494 | } |
495 | |
496 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1[1.1.1.1*]]]]" )); |
497 | QCOMPARE(spy.mSignals, QStringList() |
498 | << QStringLiteral("rowsAboutToBeInserted(1)" ) |
499 | << QStringLiteral("rowsInserted(1)" )); |
500 | } |
501 | |
502 | void testInsertIntoVisibleWithChildren() |
503 | { |
504 | QStandardItemModel model; |
505 | const QString sourceStr = QStringLiteral("[1[1.1[1.1.1*]]]" ); |
506 | fillModel(model, str: sourceStr); |
507 | QCOMPARE(treeAsString(model), sourceStr); |
508 | |
509 | TestModel proxy(&model); |
510 | QCOMPARE(treeAsString(proxy), sourceStr); |
511 | |
512 | ModelSignalSpy spy(proxy); |
513 | { |
514 | QStandardItem *item_1_1_2 = new QStandardItem(QStringLiteral("1.1.2" )); |
515 | QStandardItem *item_1_1_2_1 = new QStandardItem(QStringLiteral("1.1.2.1" )); |
516 | item_1_1_2_1->setData(value: true, role: s_filterRole); |
517 | item_1_1_2->appendRow(aitem: item_1_1_2_1); |
518 | |
519 | QStandardItem *item_1_1 = model.item(row: 0)->child(row: 0); |
520 | item_1_1->appendRow(aitem: item_1_1_2); |
521 | } |
522 | |
523 | QCOMPARE(treeAsString(proxy), QStringLiteral("[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" )); |
524 | QCOMPARE(spy.mSignals, QStringList() |
525 | << QStringLiteral("rowsAboutToBeInserted(1.1.2)" ) |
526 | << QStringLiteral("rowsInserted(1.1.2)" )); |
527 | } |
528 | |
529 | void testInsertBefore() |
530 | { |
531 | QStandardItemModel model; |
532 | const QString sourceStr = "[1[1.1[1.1.2*]]]" ; |
533 | fillModel(model, str: sourceStr); |
534 | QCOMPARE(treeAsString(model), sourceStr); |
535 | |
536 | TestModel proxy(&model); |
537 | QCOMPARE(treeAsString(proxy), sourceStr); |
538 | |
539 | ModelSignalSpy spy(proxy); |
540 | { |
541 | QStandardItem *item_1_1_1 = new QStandardItem("1.1.1" ); |
542 | |
543 | QStandardItem *item_1_1 = model.item(row: 0)->child(row: 0); |
544 | item_1_1->insertRow(arow: 0, aitem: item_1_1_1); |
545 | } |
546 | |
547 | QCOMPARE(treeAsString(proxy), QString("[1[1.1[1.1.2*]]]" )); |
548 | QCOMPARE(spy.mSignals, QStringList()); |
549 | } |
550 | |
551 | void testInsertHidden() // inserting filtered-out rows shouldn't emit anything |
552 | { |
553 | QStandardItemModel model; |
554 | const QString sourceStr = QStringLiteral("[1[1.1]]" ); |
555 | fillModel(model, str: sourceStr); |
556 | QCOMPARE(treeAsString(model), sourceStr); |
557 | |
558 | TestModel proxy(&model); |
559 | QCOMPARE(treeAsString(proxy), QString()); |
560 | |
561 | ModelSignalSpy spy(proxy); |
562 | { |
563 | QStandardItem *item_1_1_1 = new QStandardItem(QStringLiteral("1.1.1" )); |
564 | QStandardItem *item_1_1_1_1 = new QStandardItem(QStringLiteral("1.1.1.1" )); |
565 | item_1_1_1->appendRow(aitem: item_1_1_1_1); |
566 | |
567 | QStandardItem *item_1_1 = model.item(row: 0)->child(row: 0); |
568 | item_1_1->appendRow(aitem: item_1_1_1); |
569 | } |
570 | |
571 | QCOMPARE(treeAsString(proxy), QString()); |
572 | QCOMPARE(spy.mSignals, QStringList()); |
573 | } |
574 | |
575 | void testConsecutiveInserts_data() |
576 | { |
577 | testInitialFiltering_data(); |
578 | } |
579 | |
580 | void testConsecutiveInserts() |
581 | { |
582 | QFETCH(QString, sourceStr); |
583 | QFETCH(QString, proxyStr); |
584 | |
585 | QStandardItemModel model; |
586 | TestModel proxy(&model); // this time the proxy listens to the model while we fill it |
587 | |
588 | fillModel(model, str: sourceStr); |
589 | QCOMPARE(treeAsString(model), sourceStr); |
590 | QCOMPARE(treeAsString(proxy), proxyStr); |
591 | } |
592 | |
593 | void testRemove_data() |
594 | { |
595 | QTest::addColumn<QString>(name: "sourceStr" ); |
596 | QTest::addColumn<QString>(name: "initialProxyStr" ); |
597 | QTest::addColumn<QString>(name: "remove" ); // remove this item |
598 | QTest::addColumn<QString>(name: "expectedProxyStr" ); |
599 | QTest::addColumn<QStringList>(name: "expectedSignals" ); |
600 | |
601 | const QStringList remove1_1_1 = (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) << QStringLiteral("rowsRemoved(1.1.1)" )); |
602 | |
603 | QTest::newRow(dataTag: "toplevel" ) << "[1* 2* 3*]" << "[1* 2* 3*]" << "1" << "[2* 3*]" |
604 | << (QStringList() << QStringLiteral("rowsAboutToBeRemoved(1)" ) << QStringLiteral("rowsRemoved(1)" )); |
605 | |
606 | QTest::newRow(dataTag: "remove_hidden" ) << "[1 2* 3*]" << "[2* 3*]" << "1" << "[2* 3*]" << QStringList(); |
607 | |
608 | QTest::newRow(dataTag: "parent_hidden" ) << "[1[1.1[1.1.1]]]" << "" << "1.1.1" << "" << QStringList(); |
609 | |
610 | QTest::newRow(dataTag: "child_hidden" ) << "[1[1.1*[1.1.1]]]" << "[1[1.1*]]" << "1.1.1" << "[1[1.1*]]" << QStringList(); |
611 | |
612 | QTest::newRow(dataTag: "parent_visible" ) << "[1[1.1*[1.1.1*]]]" << "[1[1.1*[1.1.1*]]]" << "1.1.1" << "[1[1.1*]]" |
613 | << remove1_1_1; |
614 | |
615 | QTest::newRow(dataTag: "visible" ) << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" << "1.1.1" << "[1[1.1[1.1.2*]]]" |
616 | << remove1_1_1; |
617 | QTest::newRow(dataTag: "visible_cousin" ) << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "[1[1.1[1.1.1* 1.1.2[1.1.2.1*]]]]" << "1.1.1" << "[1[1.1[1.1.2[1.1.2.1*]]]]" |
618 | << remove1_1_1; |
619 | |
620 | // The following tests trigger the removal of an ascendant. |
621 | // We could optimize the rows{AboutToBe,}Removed(1.1.1) away... |
622 | |
623 | QTest::newRow(dataTag: "remove_parent" ) << "[1[1.1[1.1.1* 1.1.2] 1.2*]]" << "[1[1.1[1.1.1*] 1.2*]]" << "1.1.1" << "[1[1.2*]]" |
624 | << (QStringList() |
625 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
626 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
627 | << QStringLiteral("rowsAboutToBeRemoved(1.1)" ) |
628 | << QStringLiteral("rowsRemoved(1.1)" ) |
629 | << QStringLiteral("dataChanged(1)" )); |
630 | |
631 | QTest::newRow(dataTag: "with_children" ) << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "[1[1.1[1.1.1[1.1.1.1*]]] 2*]" << "1.1.1" << "[2*]" |
632 | << (QStringList() |
633 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
634 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
635 | << QStringLiteral("rowsAboutToBeRemoved(1)" ) |
636 | << QStringLiteral("rowsRemoved(1)" )); |
637 | |
638 | QTest::newRow(dataTag: "last_visible" ) << "[1[1.1[1.1.1* 1.1.2]]]" << "[1[1.1[1.1.1*]]]" << "1.1.1" << "" |
639 | << (QStringList() |
640 | << QStringLiteral("rowsAboutToBeRemoved(1.1.1)" ) |
641 | << QStringLiteral("rowsRemoved(1.1.1)" ) |
642 | << QStringLiteral("rowsAboutToBeRemoved(1)" ) |
643 | << QStringLiteral("rowsRemoved(1)" )); |
644 | |
645 | |
646 | } |
647 | |
648 | void testRemove() |
649 | { |
650 | QFETCH(QString, sourceStr); |
651 | QFETCH(QString, initialProxyStr); |
652 | QFETCH(QString, remove); |
653 | QFETCH(QString, expectedProxyStr); |
654 | QFETCH(QStringList, expectedSignals); |
655 | |
656 | QStandardItemModel model; |
657 | fillModel(model, str: sourceStr); |
658 | QCOMPARE(treeAsString(model), sourceStr); |
659 | |
660 | TestModel proxy(&model); |
661 | QCOMPARE(treeAsString(proxy), initialProxyStr); |
662 | |
663 | ModelSignalSpy spy(proxy); |
664 | QStandardItem *itemToRemove = itemByText(model, text: remove); |
665 | QVERIFY(itemToRemove); |
666 | if (itemToRemove->parent()) |
667 | itemToRemove->parent()->removeRow(row: itemToRemove->row()); |
668 | else |
669 | model.removeRow(arow: itemToRemove->row()); |
670 | QCOMPARE(treeAsString(proxy), expectedProxyStr); |
671 | |
672 | //qDebug() << spy.mSignals; |
673 | QCOMPARE(spy.mSignals, expectedSignals); |
674 | } |
675 | |
676 | void testStandardFiltering_data() |
677 | { |
678 | QTest::addColumn<QString>(name: "sourceStr" ); |
679 | QTest::addColumn<QString>(name: "initialProxyStr" ); |
680 | QTest::addColumn<QString>(name: "filter" ); |
681 | QTest::addColumn<QString>(name: "expectedProxyStr" ); |
682 | |
683 | QTest::newRow(dataTag: "select_child" ) << "[1[1.1[1.1.1* 1.1.2*]]]" << "[1[1.1[1.1.1* 1.1.2*]]]" |
684 | << "1.1.2" << "[1[1.1[1.1.2*]]]" ; |
685 | |
686 | QTest::newRow(dataTag: "filter_all_out" ) << "[1[1.1[1.1.1*]]]" << "[1[1.1[1.1.1*]]]" |
687 | << "test" << "" ; |
688 | |
689 | QTest::newRow(dataTag: "select_parent" ) << "[1[1.1[1.1.1*[child*] 1.1.2*]]]" << "[1[1.1[1.1.1*[child*] 1.1.2*]]]" |
690 | << "1.1.1" << "[1[1.1[1.1.1*]]]" ; |
691 | |
692 | } |
693 | |
694 | void testStandardFiltering() |
695 | { |
696 | QFETCH(QString, sourceStr); |
697 | QFETCH(QString, initialProxyStr); |
698 | QFETCH(QString, filter); |
699 | QFETCH(QString, expectedProxyStr); |
700 | |
701 | QStandardItemModel model; |
702 | fillModel(model, str: sourceStr); |
703 | QCOMPARE(treeAsString(model), sourceStr); |
704 | |
705 | TestModel proxy(&model); |
706 | QCOMPARE(treeAsString(proxy), initialProxyStr); |
707 | |
708 | ModelSignalSpy spy(proxy); |
709 | |
710 | //qDebug() << "setFilterFixedString"; |
711 | proxy.setFilterRole(Qt::DisplayRole); |
712 | proxy.setFilterFixedString(filter); |
713 | |
714 | QCOMPARE(treeAsString(proxy), expectedProxyStr); |
715 | |
716 | } |
717 | |
718 | private: |
719 | QStandardItem *itemByText(const QStandardItemModel& model, const QString &text) const { |
720 | QModelIndexList list = model.match(start: model.index(row: 0, column: 0), role: Qt::DisplayRole, value: text, hits: 1, flags: Qt::MatchRecursive); |
721 | return list.isEmpty() ? 0 : model.itemFromIndex(index: list.first()); |
722 | } |
723 | }; |
724 | |
725 | QTEST_GUILESS_MAIN(tst_QSortFilterProxyModel_Recursive) |
726 | #include "tst_qsortfilterproxymodel_recursive.moc" |
727 | |