1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
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 <QComboBox> |
30 | #include <QDataWidgetMapper> |
31 | #include <QLineEdit> |
32 | #include <QMetaType> |
33 | #include <QStandardItemModel> |
34 | #include <QSignalSpy> |
35 | #include <QTest> |
36 | #include <QTextEdit> |
37 | #include <QVBoxLayout> |
38 | |
39 | class tst_QDataWidgetMapper: public QObject |
40 | { |
41 | Q_OBJECT |
42 | private slots: |
43 | void initTestCase(); |
44 | |
45 | void setModel(); |
46 | void navigate(); |
47 | void addMapping(); |
48 | void currentIndexChanged(); |
49 | void changingValues(); |
50 | void setData(); |
51 | void mappedWidgetAt(); |
52 | |
53 | void comboBox(); |
54 | |
55 | void textEditDoesntChangeFocusOnTab_qtbug3305(); |
56 | }; |
57 | |
58 | Q_DECLARE_METATYPE(QAbstractItemDelegate::EndEditHint) |
59 | |
60 | static QStandardItemModel *testModel(QObject *parent) |
61 | { |
62 | QStandardItemModel *model = new QStandardItemModel(10, 10, parent); |
63 | |
64 | for (int row = 0; row < 10; ++row) { |
65 | const QString prefix = QLatin1String("item " ) + QString::number(row) |
66 | + QLatin1Char(' '); |
67 | for (int col = 0; col < 10; ++col) |
68 | model->setData(index: model->index(row, column: col), value: prefix + QString::number(col)); |
69 | } |
70 | |
71 | return model; |
72 | } |
73 | |
74 | void tst_QDataWidgetMapper::initTestCase() |
75 | { |
76 | qRegisterMetaType<QAbstractItemDelegate::EndEditHint>(); |
77 | } |
78 | |
79 | void tst_QDataWidgetMapper::setModel() |
80 | { |
81 | QDataWidgetMapper mapper; |
82 | |
83 | QCOMPARE(mapper.model(), nullptr); |
84 | |
85 | { // let the model go out of scope firstma |
86 | QStandardItemModel model; |
87 | mapper.setModel(&model); |
88 | QCOMPARE(mapper.model(), &model); |
89 | } |
90 | |
91 | QCOMPARE(mapper.model(), nullptr); |
92 | |
93 | { // let the mapper go out of scope first |
94 | QStandardItemModel model2; |
95 | QDataWidgetMapper mapper2; |
96 | mapper2.setModel(&model2); |
97 | } |
98 | } |
99 | |
100 | void tst_QDataWidgetMapper::navigate() |
101 | { |
102 | QDataWidgetMapper mapper; |
103 | QAbstractItemModel *model = testModel(parent: &mapper); |
104 | mapper.setModel(model); |
105 | |
106 | QLineEdit edit1; |
107 | QLineEdit edit2; |
108 | QLineEdit edit3; |
109 | |
110 | mapper.addMapping(widget: &edit1, section: 0); |
111 | mapper.toFirst(); |
112 | mapper.addMapping(widget: &edit2, section: 1); |
113 | mapper.addMapping(widget: &edit3, section: 2); |
114 | |
115 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
116 | QVERIFY(edit2.text().isEmpty()); |
117 | QVERIFY(edit3.text().isEmpty()); |
118 | QVERIFY(mapper.submit()); |
119 | edit2.setText(QString("item 0 1" )); |
120 | edit3.setText(QString("item 0 2" )); |
121 | QVERIFY(mapper.submit()); |
122 | |
123 | mapper.toFirst(); //this will repopulate |
124 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
125 | QCOMPARE(edit2.text(), QString("item 0 1" )); |
126 | QCOMPARE(edit3.text(), QString("item 0 2" )); |
127 | |
128 | |
129 | mapper.toFirst(); |
130 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
131 | QCOMPARE(edit2.text(), QString("item 0 1" )); |
132 | QCOMPARE(edit3.text(), QString("item 0 2" )); |
133 | |
134 | mapper.toPrevious(); // should do nothing |
135 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
136 | QCOMPARE(edit2.text(), QString("item 0 1" )); |
137 | QCOMPARE(edit3.text(), QString("item 0 2" )); |
138 | |
139 | mapper.toNext(); |
140 | QCOMPARE(edit1.text(), QString("item 1 0" )); |
141 | QCOMPARE(edit2.text(), QString("item 1 1" )); |
142 | QCOMPARE(edit3.text(), QString("item 1 2" )); |
143 | |
144 | mapper.toLast(); |
145 | QCOMPARE(edit1.text(), QString("item 9 0" )); |
146 | QCOMPARE(edit2.text(), QString("item 9 1" )); |
147 | QCOMPARE(edit3.text(), QString("item 9 2" )); |
148 | |
149 | mapper.toNext(); // should do nothing |
150 | QCOMPARE(edit1.text(), QString("item 9 0" )); |
151 | QCOMPARE(edit2.text(), QString("item 9 1" )); |
152 | QCOMPARE(edit3.text(), QString("item 9 2" )); |
153 | |
154 | mapper.setCurrentIndex(4); |
155 | QCOMPARE(edit1.text(), QString("item 4 0" )); |
156 | QCOMPARE(edit2.text(), QString("item 4 1" )); |
157 | QCOMPARE(edit3.text(), QString("item 4 2" )); |
158 | |
159 | mapper.setCurrentIndex(-1); // should do nothing |
160 | QCOMPARE(edit1.text(), QString("item 4 0" )); |
161 | QCOMPARE(edit2.text(), QString("item 4 1" )); |
162 | QCOMPARE(edit3.text(), QString("item 4 2" )); |
163 | |
164 | mapper.setCurrentIndex(10); // should do nothing |
165 | QCOMPARE(edit1.text(), QString("item 4 0" )); |
166 | QCOMPARE(edit2.text(), QString("item 4 1" )); |
167 | QCOMPARE(edit3.text(), QString("item 4 2" )); |
168 | |
169 | mapper.setCurrentModelIndex(QModelIndex()); // should do nothing |
170 | QCOMPARE(edit1.text(), QString("item 4 0" )); |
171 | QCOMPARE(edit2.text(), QString("item 4 1" )); |
172 | QCOMPARE(edit3.text(), QString("item 4 2" )); |
173 | |
174 | mapper.setCurrentModelIndex(model->index(row: 6, column: 0)); |
175 | QCOMPARE(edit1.text(), QString("item 6 0" )); |
176 | QCOMPARE(edit2.text(), QString("item 6 1" )); |
177 | QCOMPARE(edit3.text(), QString("item 6 2" )); |
178 | |
179 | /* now try vertical navigation */ |
180 | |
181 | mapper.setOrientation(Qt::Vertical); |
182 | |
183 | mapper.addMapping(widget: &edit1, section: 0); |
184 | mapper.addMapping(widget: &edit2, section: 1); |
185 | mapper.addMapping(widget: &edit3, section: 2); |
186 | |
187 | mapper.toFirst(); |
188 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
189 | QCOMPARE(edit2.text(), QString("item 1 0" )); |
190 | QCOMPARE(edit3.text(), QString("item 2 0" )); |
191 | |
192 | mapper.toPrevious(); // should do nothing |
193 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
194 | QCOMPARE(edit2.text(), QString("item 1 0" )); |
195 | QCOMPARE(edit3.text(), QString("item 2 0" )); |
196 | |
197 | mapper.toNext(); |
198 | QCOMPARE(edit1.text(), QString("item 0 1" )); |
199 | QCOMPARE(edit2.text(), QString("item 1 1" )); |
200 | QCOMPARE(edit3.text(), QString("item 2 1" )); |
201 | |
202 | mapper.toLast(); |
203 | QCOMPARE(edit1.text(), QString("item 0 9" )); |
204 | QCOMPARE(edit2.text(), QString("item 1 9" )); |
205 | QCOMPARE(edit3.text(), QString("item 2 9" )); |
206 | |
207 | mapper.toNext(); // should do nothing |
208 | QCOMPARE(edit1.text(), QString("item 0 9" )); |
209 | QCOMPARE(edit2.text(), QString("item 1 9" )); |
210 | QCOMPARE(edit3.text(), QString("item 2 9" )); |
211 | |
212 | mapper.setCurrentIndex(4); |
213 | QCOMPARE(edit1.text(), QString("item 0 4" )); |
214 | QCOMPARE(edit2.text(), QString("item 1 4" )); |
215 | QCOMPARE(edit3.text(), QString("item 2 4" )); |
216 | |
217 | mapper.setCurrentIndex(-1); // should do nothing |
218 | QCOMPARE(edit1.text(), QString("item 0 4" )); |
219 | QCOMPARE(edit2.text(), QString("item 1 4" )); |
220 | QCOMPARE(edit3.text(), QString("item 2 4" )); |
221 | |
222 | mapper.setCurrentIndex(10); // should do nothing |
223 | QCOMPARE(edit1.text(), QString("item 0 4" )); |
224 | QCOMPARE(edit2.text(), QString("item 1 4" )); |
225 | QCOMPARE(edit3.text(), QString("item 2 4" )); |
226 | |
227 | mapper.setCurrentModelIndex(QModelIndex()); // should do nothing |
228 | QCOMPARE(edit1.text(), QString("item 0 4" )); |
229 | QCOMPARE(edit2.text(), QString("item 1 4" )); |
230 | QCOMPARE(edit3.text(), QString("item 2 4" )); |
231 | |
232 | mapper.setCurrentModelIndex(model->index(row: 0, column: 6)); |
233 | QCOMPARE(edit1.text(), QString("item 0 6" )); |
234 | QCOMPARE(edit2.text(), QString("item 1 6" )); |
235 | QCOMPARE(edit3.text(), QString("item 2 6" )); |
236 | } |
237 | |
238 | void tst_QDataWidgetMapper::addMapping() |
239 | { |
240 | QDataWidgetMapper mapper; |
241 | QAbstractItemModel *model = testModel(parent: &mapper); |
242 | mapper.setModel(model); |
243 | |
244 | QLineEdit edit1; |
245 | mapper.addMapping(widget: &edit1, section: 0); |
246 | mapper.toFirst(); |
247 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
248 | |
249 | mapper.addMapping(widget: &edit1, section: 1); |
250 | mapper.toFirst(); |
251 | QCOMPARE(edit1.text(), QString("item 0 1" )); |
252 | |
253 | QCOMPARE(mapper.mappedSection(&edit1), 1); |
254 | |
255 | edit1.clear(); |
256 | mapper.removeMapping(widget: &edit1); |
257 | mapper.toFirst(); |
258 | QCOMPARE(edit1.text(), QString()); |
259 | |
260 | { |
261 | QLineEdit edit2; |
262 | mapper.addMapping(widget: &edit2, section: 2); |
263 | mapper.toFirst(); |
264 | QCOMPARE(edit2.text(), QString("item 0 2" )); |
265 | } // let the edit go out of scope |
266 | |
267 | QCOMPARE(mapper.mappedWidgetAt(2), nullptr); |
268 | mapper.toLast(); |
269 | } |
270 | |
271 | void tst_QDataWidgetMapper::currentIndexChanged() |
272 | { |
273 | QDataWidgetMapper mapper; |
274 | QAbstractItemModel *model = testModel(parent: &mapper); |
275 | mapper.setModel(model); |
276 | |
277 | QSignalSpy spy(&mapper, &QDataWidgetMapper::currentIndexChanged); |
278 | |
279 | mapper.toFirst(); |
280 | QCOMPARE(spy.count(), 1); |
281 | QCOMPARE(spy.takeFirst().at(0).toInt(), 0); |
282 | |
283 | mapper.toNext(); |
284 | QCOMPARE(spy.count(), 1); |
285 | QCOMPARE(spy.takeFirst().at(0).toInt(), 1); |
286 | |
287 | mapper.setCurrentIndex(7); |
288 | QCOMPARE(spy.count(), 1); |
289 | QCOMPARE(spy.takeFirst().at(0).toInt(), 7); |
290 | |
291 | mapper.setCurrentIndex(-1); |
292 | QCOMPARE(spy.count(), 0); |
293 | |
294 | mapper.setCurrentIndex(42); |
295 | QCOMPARE(spy.count(), 0); |
296 | } |
297 | |
298 | void tst_QDataWidgetMapper::changingValues() |
299 | { |
300 | QDataWidgetMapper mapper; |
301 | QAbstractItemModel *model = testModel(parent: &mapper); |
302 | mapper.setModel(model); |
303 | |
304 | QLineEdit edit1; |
305 | mapper.addMapping(widget: &edit1, section: 0); |
306 | mapper.toFirst(); |
307 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
308 | |
309 | QLineEdit edit2; |
310 | mapper.addMapping(widget: &edit2, section: 0, propertyName: "text" ); |
311 | mapper.toFirst(); |
312 | QCOMPARE(edit2.text(), QString("item 0 0" )); |
313 | |
314 | model->setData(index: model->index(row: 0, column: 0), value: QString("changed" )); |
315 | QCOMPARE(edit1.text(), QString("changed" )); |
316 | QCOMPARE(edit2.text(), QString("changed" )); |
317 | } |
318 | |
319 | void tst_QDataWidgetMapper::setData() |
320 | { |
321 | QDataWidgetMapper mapper; |
322 | QAbstractItemModel *model = testModel(parent: &mapper); |
323 | mapper.setModel(model); |
324 | |
325 | QLineEdit edit1; |
326 | QLineEdit edit2; |
327 | QLineEdit edit3; |
328 | |
329 | mapper.addMapping(widget: &edit1, section: 0); |
330 | mapper.addMapping(widget: &edit2, section: 1); |
331 | mapper.addMapping(widget: &edit3, section: 0, propertyName: "text" ); |
332 | mapper.toFirst(); |
333 | QCOMPARE(edit1.text(), QString("item 0 0" )); |
334 | QCOMPARE(edit2.text(), QString("item 0 1" )); |
335 | QCOMPARE(edit3.text(), QString("item 0 0" )); |
336 | |
337 | edit1.setText("new text" ); |
338 | |
339 | mapper.submit(); |
340 | QCOMPARE(model->data(model->index(0, 0)).toString(), QString("new text" )); |
341 | |
342 | edit3.setText("more text" ); |
343 | |
344 | mapper.submit(); |
345 | QCOMPARE(model->data(model->index(0, 0)).toString(), QString("more text" )); |
346 | } |
347 | |
348 | void tst_QDataWidgetMapper::comboBox() |
349 | { |
350 | QDataWidgetMapper mapper; |
351 | QAbstractItemModel *model = testModel(parent: &mapper); |
352 | mapper.setModel(model); |
353 | mapper.setSubmitPolicy(QDataWidgetMapper::ManualSubmit); |
354 | |
355 | QComboBox readOnlyBox; |
356 | readOnlyBox.setEditable(false); |
357 | readOnlyBox.addItem(atext: "read only item 0" ); |
358 | readOnlyBox.addItem(atext: "read only item 1" ); |
359 | readOnlyBox.addItem(atext: "read only item 2" ); |
360 | |
361 | QComboBox readWriteBox; |
362 | readWriteBox.setEditable(true); |
363 | readWriteBox.addItem(atext: "read write item 0" ); |
364 | readWriteBox.addItem(atext: "read write item 1" ); |
365 | readWriteBox.addItem(atext: "read write item 2" ); |
366 | |
367 | // populate the combo boxes with data |
368 | mapper.addMapping(widget: &readOnlyBox, section: 0, propertyName: "currentIndex" ); |
369 | mapper.addMapping(widget: &readWriteBox, section: 1, propertyName: "currentText" ); |
370 | mapper.toFirst(); |
371 | |
372 | // setCurrentIndex caused the value at index 0 to be displayed |
373 | QCOMPARE(readOnlyBox.currentText(), QString("read only item 0" )); |
374 | // setCurrentText set the value in the line edit since the combobox is editable |
375 | QCOMPARE(readWriteBox.currentText(), QString("item 0 1" )); |
376 | |
377 | // set some new values on the boxes |
378 | readOnlyBox.setCurrentIndex(1); |
379 | readWriteBox.setEditText("read write item y" ); |
380 | |
381 | mapper.submit(); |
382 | |
383 | // make sure the new values are in the model |
384 | QCOMPARE(model->data(model->index(0, 0)).toInt(), 1); |
385 | QCOMPARE(model->data(model->index(0, 1)).toString(), QString("read write item y" )); |
386 | |
387 | // now test updating of the widgets |
388 | model->setData(index: model->index(row: 0, column: 0), value: 2, role: Qt::EditRole); |
389 | model->setData(index: model->index(row: 0, column: 1), value: QString("read write item z" ), role: Qt::EditRole); |
390 | |
391 | QCOMPARE(readOnlyBox.currentIndex(), 2); |
392 | QCOMPARE(readWriteBox.currentText(), QString("read write item z" )); |
393 | } |
394 | |
395 | void tst_QDataWidgetMapper::mappedWidgetAt() |
396 | { |
397 | QDataWidgetMapper mapper; |
398 | QAbstractItemModel *model = testModel(parent: &mapper); |
399 | mapper.setModel(model); |
400 | |
401 | QLineEdit lineEdit1; |
402 | QLineEdit lineEdit2; |
403 | |
404 | QCOMPARE(mapper.mappedWidgetAt(432312), nullptr); |
405 | |
406 | mapper.addMapping(widget: &lineEdit1, section: 1); |
407 | mapper.addMapping(widget: &lineEdit2, section: 2); |
408 | |
409 | QCOMPARE(mapper.mappedWidgetAt(1), &lineEdit1); |
410 | QCOMPARE(mapper.mappedWidgetAt(2), &lineEdit2); |
411 | |
412 | mapper.addMapping(widget: &lineEdit2, section: 4242); |
413 | |
414 | QCOMPARE(mapper.mappedWidgetAt(2), nullptr); |
415 | QCOMPARE(mapper.mappedWidgetAt(4242), &lineEdit2); |
416 | } |
417 | |
418 | void tst_QDataWidgetMapper::textEditDoesntChangeFocusOnTab_qtbug3305() |
419 | { |
420 | if (QGuiApplication::platformName().startsWith(s: QLatin1String("wayland" ), cs: Qt::CaseInsensitive)) |
421 | QSKIP("Wayland: This fails. Figure out why." ); |
422 | |
423 | QDataWidgetMapper mapper; |
424 | QAbstractItemModel *model = testModel(parent: &mapper); |
425 | mapper.setModel(model); |
426 | |
427 | QSignalSpy closeEditorSpy(mapper.itemDelegate(), |
428 | &QAbstractItemDelegate::closeEditor); |
429 | QVERIFY(closeEditorSpy.isValid()); |
430 | |
431 | QWidget container; |
432 | container.setLayout(new QVBoxLayout); |
433 | |
434 | QLineEdit *lineEdit = new QLineEdit; |
435 | mapper.addMapping(widget: lineEdit, section: 0); |
436 | container.layout()->addWidget(w: lineEdit); |
437 | |
438 | QTextEdit *textEdit = new QTextEdit; |
439 | mapper.addMapping(widget: textEdit, section: 1); |
440 | container.layout()->addWidget(w: textEdit); |
441 | |
442 | lineEdit->setFocus(); |
443 | |
444 | container.show(); |
445 | |
446 | QApplication::setActiveWindow(&container); |
447 | QVERIFY(QTest::qWaitForWindowActive(&container)); |
448 | |
449 | int closeEditorSpyCount = 0; |
450 | const QString textEditContents = textEdit->toPlainText(); |
451 | |
452 | QCOMPARE(closeEditorSpy.count(), closeEditorSpyCount); |
453 | QVERIFY(lineEdit->hasFocus()); |
454 | QVERIFY(!textEdit->hasFocus()); |
455 | |
456 | // this will generate a closeEditor for the tab key, and another for the focus out |
457 | QTest::keyClick(widget: QApplication::focusWidget(), key: Qt::Key_Tab); |
458 | closeEditorSpyCount += 2; |
459 | QTRY_COMPARE(closeEditorSpy.count(), closeEditorSpyCount); |
460 | |
461 | QTRY_VERIFY(textEdit->hasFocus()); |
462 | QVERIFY(!lineEdit->hasFocus()); |
463 | |
464 | // now that the text edit is focused, a tab keypress will insert a tab, not change focus |
465 | QTest::keyClick(widget: QApplication::focusWidget(), key: Qt::Key_Tab); |
466 | QTRY_COMPARE(closeEditorSpy.count(), closeEditorSpyCount); |
467 | |
468 | QVERIFY(!lineEdit->hasFocus()); |
469 | QVERIFY(textEdit->hasFocus()); |
470 | QCOMPARE(textEdit->toPlainText(), QLatin1Char('\t') + textEditContents); |
471 | |
472 | // now give focus back to the line edit and check closeEditor gets emitted |
473 | lineEdit->setFocus(); |
474 | QTRY_VERIFY(lineEdit->hasFocus()); |
475 | QVERIFY(!textEdit->hasFocus()); |
476 | ++closeEditorSpyCount; |
477 | QCOMPARE(closeEditorSpy.count(), closeEditorSpyCount); |
478 | } |
479 | |
480 | QTEST_MAIN(tst_QDataWidgetMapper) |
481 | #include "tst_qdatawidgetmapper.moc" |
482 | |