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#include <QtTest/QtTest>
29#include <QUndoGroup>
30#include <QUndoStack>
31#include <QAction>
32
33/******************************************************************************
34** Commands
35*/
36
37class InsertCommand : public QUndoCommand
38{
39public:
40 InsertCommand(QString *str, int idx, const QString &text,
41 QUndoCommand *parent = 0);
42
43 virtual void undo();
44 virtual void redo();
45
46private:
47 QString *m_str;
48 int m_idx;
49 QString m_text;
50};
51
52class RemoveCommand : public QUndoCommand
53{
54public:
55 RemoveCommand(QString *str, int idx, int len, QUndoCommand *parent = 0);
56
57 virtual void undo();
58 virtual void redo();
59
60private:
61 QString *m_str;
62 int m_idx;
63 QString m_text;
64};
65
66class AppendCommand : public QUndoCommand
67{
68public:
69 AppendCommand(QString *str, const QString &text, QUndoCommand *parent = 0);
70
71 virtual void undo();
72 virtual void redo();
73 virtual int id() const;
74 virtual bool mergeWith(const QUndoCommand *other);
75
76 bool merged;
77
78private:
79 QString *m_str;
80 QString m_text;
81};
82
83InsertCommand::InsertCommand(QString *str, int idx, const QString &text,
84 QUndoCommand *parent)
85 : QUndoCommand(parent)
86{
87 QVERIFY(str->length() >= idx);
88
89 setText("insert");
90
91 m_str = str;
92 m_idx = idx;
93 m_text = text;
94}
95
96void InsertCommand::redo()
97{
98 QVERIFY(m_str->length() >= m_idx);
99
100 m_str->insert(i: m_idx, s: m_text);
101}
102
103void InsertCommand::undo()
104{
105 QCOMPARE(m_str->mid(m_idx, m_text.length()), m_text);
106
107 m_str->remove(i: m_idx, len: m_text.length());
108}
109
110RemoveCommand::RemoveCommand(QString *str, int idx, int len, QUndoCommand *parent)
111 : QUndoCommand(parent)
112{
113 QVERIFY(str->length() >= idx + len);
114
115 setText("remove");
116
117 m_str = str;
118 m_idx = idx;
119 m_text = m_str->mid(position: m_idx, n: len);
120}
121
122void RemoveCommand::redo()
123{
124 QCOMPARE(m_str->mid(m_idx, m_text.length()), m_text);
125
126 m_str->remove(i: m_idx, len: m_text.length());
127}
128
129void RemoveCommand::undo()
130{
131 QVERIFY(m_str->length() >= m_idx);
132
133 m_str->insert(i: m_idx, s: m_text);
134}
135
136AppendCommand::AppendCommand(QString *str, const QString &text, QUndoCommand *parent)
137 : QUndoCommand(parent)
138{
139 setText("append");
140
141 m_str = str;
142 m_text = text;
143 merged = false;
144}
145
146void AppendCommand::redo()
147{
148 m_str->append(s: m_text);
149}
150
151void AppendCommand::undo()
152{
153 QCOMPARE(m_str->mid(m_str->length() - m_text.length()), m_text);
154
155 m_str->truncate(pos: m_str->length() - m_text.length());
156}
157
158int AppendCommand::id() const
159{
160 return 1;
161}
162
163bool AppendCommand::mergeWith(const QUndoCommand *other)
164{
165 if (other->id() != id())
166 return false;
167 m_text += static_cast<const AppendCommand*>(other)->m_text;
168 merged = true;
169 return true;
170}
171
172/******************************************************************************
173** tst_QUndoStack
174*/
175
176class tst_QUndoGroup : public QObject
177{
178 Q_OBJECT
179public:
180 tst_QUndoGroup();
181
182private slots:
183 void setActive();
184 void addRemoveStack();
185 void deleteStack();
186 void checkSignals();
187 void addStackAndDie();
188 void commandTextFormat();
189};
190
191tst_QUndoGroup::tst_QUndoGroup()
192{
193}
194
195void tst_QUndoGroup::setActive()
196{
197 QUndoGroup group;
198 QUndoStack stack1(&group), stack2(&group);
199
200 QCOMPARE(group.activeStack(), nullptr);
201 QCOMPARE(stack1.isActive(), false);
202 QCOMPARE(stack2.isActive(), false);
203
204 QUndoStack stack3;
205 QCOMPARE(stack3.isActive(), true);
206
207 group.addStack(stack: &stack3);
208 QCOMPARE(stack3.isActive(), false);
209
210 stack1.setActive();
211 QCOMPARE(group.activeStack(), &stack1);
212 QCOMPARE(stack1.isActive(), true);
213 QCOMPARE(stack2.isActive(), false);
214 QCOMPARE(stack3.isActive(), false);
215
216 group.setActiveStack(&stack2);
217 QCOMPARE(group.activeStack(), &stack2);
218 QCOMPARE(stack1.isActive(), false);
219 QCOMPARE(stack2.isActive(), true);
220 QCOMPARE(stack3.isActive(), false);
221
222 group.removeStack(stack: &stack2);
223 QCOMPARE(group.activeStack(), nullptr);
224 QCOMPARE(stack1.isActive(), false);
225 QCOMPARE(stack2.isActive(), true);
226 QCOMPARE(stack3.isActive(), false);
227
228 group.removeStack(stack: &stack2);
229 QCOMPARE(group.activeStack(), nullptr);
230 QCOMPARE(stack1.isActive(), false);
231 QCOMPARE(stack2.isActive(), true);
232 QCOMPARE(stack3.isActive(), false);
233}
234
235void tst_QUndoGroup::addRemoveStack()
236{
237 QUndoGroup group;
238
239 QUndoStack stack1(&group);
240 QCOMPARE(group.stacks(), QList<QUndoStack*>() << &stack1);
241
242 QUndoStack stack2;
243 group.addStack(stack: &stack2);
244 QCOMPARE(group.stacks(), QList<QUndoStack*>() << &stack1 << &stack2);
245
246 group.addStack(stack: &stack1);
247 QCOMPARE(group.stacks(), QList<QUndoStack*>() << &stack1 << &stack2);
248
249 group.removeStack(stack: &stack1);
250 QCOMPARE(group.stacks(), QList<QUndoStack*>() << &stack2);
251
252 group.removeStack(stack: &stack1);
253 QCOMPARE(group.stacks(), QList<QUndoStack*>() << &stack2);
254
255 group.removeStack(stack: &stack2);
256 QVERIFY(group.stacks().isEmpty());
257}
258
259void tst_QUndoGroup::deleteStack()
260{
261 QUndoGroup group;
262
263 QUndoStack *stack1 = new QUndoStack(&group);
264 QCOMPARE(group.stacks(), QList<QUndoStack*>() << stack1);
265 QCOMPARE(group.activeStack(), nullptr);
266
267 stack1->setActive();
268 QCOMPARE(group.activeStack(), stack1);
269
270 QUndoStack *stack2 = new QUndoStack(&group);
271 QCOMPARE(group.stacks(), QList<QUndoStack*>() << stack1 << stack2);
272 QCOMPARE(group.activeStack(), stack1);
273
274 QUndoStack *stack3 = new QUndoStack(&group);
275 QCOMPARE(group.stacks(), QList<QUndoStack*>() << stack1 << stack2 << stack3);
276 QCOMPARE(group.activeStack(), stack1);
277
278 delete stack2;
279 QCOMPARE(group.stacks(), QList<QUndoStack*>() << stack1 << stack3);
280 QCOMPARE(group.activeStack(), stack1);
281
282 delete stack1;
283 QCOMPARE(group.stacks(), QList<QUndoStack*>() << stack3);
284 QCOMPARE(group.activeStack(), nullptr);
285
286 stack3->setActive(false);
287 QCOMPARE(group.activeStack(), nullptr);
288
289 stack3->setActive(true);
290 QCOMPARE(group.activeStack(), stack3);
291
292 group.removeStack(stack: stack3);
293 QVERIFY(group.stacks().isEmpty());
294 QCOMPARE(group.activeStack(), nullptr);
295
296 delete stack3;
297}
298
299static QString glue(const QString &s1, const QString &s2)
300{
301 QString result;
302
303 result.append(s: s1);
304 if (!s1.isEmpty() && !s2.isEmpty())
305 result.append(c: ' ');
306 result.append(s: s2);
307
308 return result;
309}
310
311#define CHECK_STATE(_activeStack, _clean, _canUndo, _undoText, _canRedo, _redoText, \
312 _cleanChanged, _indexChanged, _undoChanged, _redoChanged) \
313 QCOMPARE(group.activeStack(), (QUndoStack*)_activeStack); \
314 QCOMPARE(group.isClean(), _clean); \
315 QCOMPARE(group.canUndo(), _canUndo); \
316 QCOMPARE(group.undoText(), QString(_undoText)); \
317 QCOMPARE(group.canRedo(), _canRedo); \
318 QCOMPARE(group.redoText(), QString(_redoText)); \
319 if (_indexChanged) { \
320 QCOMPARE(indexChangedSpy.count(), 1); \
321 indexChangedSpy.clear(); \
322 } else { \
323 QCOMPARE(indexChangedSpy.count(), 0); \
324 } \
325 if (_cleanChanged) { \
326 QCOMPARE(cleanChangedSpy.count(), 1); \
327 QCOMPARE(cleanChangedSpy.at(0).at(0).toBool(), _clean); \
328 cleanChangedSpy.clear(); \
329 } else { \
330 QCOMPARE(cleanChangedSpy.count(), 0); \
331 } \
332 if (_undoChanged) { \
333 QCOMPARE(canUndoChangedSpy.count(), 1); \
334 QCOMPARE(canUndoChangedSpy.at(0).at(0).toBool(), _canUndo); \
335 QCOMPARE(undo_action->isEnabled(), _canUndo); \
336 QCOMPARE(undoTextChangedSpy.count(), 1); \
337 QCOMPARE(undoTextChangedSpy.at(0).at(0).toString(), QString(_undoText)); \
338 QCOMPARE(undo_action->text(), glue("foo", _undoText)); \
339 canUndoChangedSpy.clear(); \
340 undoTextChangedSpy.clear(); \
341 } else { \
342 QCOMPARE(canUndoChangedSpy.count(), 0); \
343 QCOMPARE(undoTextChangedSpy.count(), 0); \
344 } \
345 if (_redoChanged) { \
346 QCOMPARE(canRedoChangedSpy.count(), 1); \
347 QCOMPARE(canRedoChangedSpy.at(0).at(0).toBool(), _canRedo); \
348 QCOMPARE(redo_action->isEnabled(), _canRedo); \
349 QCOMPARE(redoTextChangedSpy.count(), 1); \
350 QCOMPARE(redoTextChangedSpy.at(0).at(0).toString(), QString(_redoText)); \
351 QCOMPARE(redo_action->text(), glue("bar", _redoText)); \
352 canRedoChangedSpy.clear(); \
353 redoTextChangedSpy.clear(); \
354 } else { \
355 QCOMPARE(canRedoChangedSpy.count(), 0); \
356 QCOMPARE(redoTextChangedSpy.count(), 0); \
357 }
358
359void tst_QUndoGroup::checkSignals()
360{
361 QUndoGroup group;
362 const QScopedPointer<QAction> undo_action(group.createUndoAction(parent: 0, prefix: QString("foo")));
363 const QScopedPointer<QAction> redo_action(group.createRedoAction(parent: 0, prefix: QString("bar")));
364 QSignalSpy indexChangedSpy(&group, &QUndoGroup::indexChanged);
365 QSignalSpy cleanChangedSpy(&group, &QUndoGroup::cleanChanged);
366 QSignalSpy canUndoChangedSpy(&group, &QUndoGroup::canUndoChanged);
367 QSignalSpy undoTextChangedSpy(&group, &QUndoGroup::undoTextChanged);
368 QSignalSpy canRedoChangedSpy(&group, &QUndoGroup::canRedoChanged);
369 QSignalSpy redoTextChangedSpy(&group, &QUndoGroup::redoTextChanged);
370
371 QString str;
372
373 CHECK_STATE(0, // activeStack
374 true, // clean
375 false, // canUndo
376 "", // undoText
377 false, // canRedo
378 "", // redoText
379 false, // cleanChanged
380 false, // indexChanged
381 false, // undoChanged
382 false) // redoChanged
383
384 group.undo();
385 CHECK_STATE(0, // activeStack
386 true, // clean
387 false, // canUndo
388 "", // undoText
389 false, // canRedo
390 "", // redoText
391 false, // cleanChanged
392 false, // indexChanged
393 false, // undoChanged
394 false) // redoChanged
395
396 group.redo();
397 CHECK_STATE(0, // activeStack
398 true, // clean
399 false, // canUndo
400 "", // undoText
401 false, // canRedo
402 "", // redoText
403 false, // cleanChanged
404 false, // indexChanged
405 false, // undoChanged
406 false) // redoChanged
407
408 QUndoStack *stack1 = new QUndoStack(&group);
409 CHECK_STATE(0, // activeStack
410 true, // clean
411 false, // canUndo
412 "", // undoText
413 false, // canRedo
414 "", // redoText
415 false, // cleanChanged
416 false, // indexChanged
417 false, // undoChanged
418 false) // redoChanged
419
420 stack1->push(cmd: new AppendCommand(&str, "foo"));
421 CHECK_STATE(0, // activeStack
422 true, // clean
423 false, // canUndo
424 "", // undoText
425 false, // canRedo
426 "", // redoText
427 false, // cleanChanged
428 false, // indexChanged
429 false, // undoChanged
430 false) // redoChanged
431
432 stack1->setActive();
433 CHECK_STATE(stack1, // activeStack
434 false, // clean
435 true, // canUndo
436 "append", // undoText
437 false, // canRedo
438 "", // redoText
439 true, // cleanChanged
440 true, // indexChanged
441 true, // undoChanged
442 true) // redoChanged
443
444 stack1->push(cmd: new InsertCommand(&str, 0, "bar"));
445 CHECK_STATE(stack1, // activeStack
446 false, // clean
447 true, // canUndo
448 "insert", // undoText
449 false, // canRedo
450 "", // redoText
451 false, // cleanChanged
452 true, // indexChanged
453 true, // undoChanged
454 true) // redoChanged
455
456 stack1->undo();
457 CHECK_STATE(stack1, // activeStack
458 false, // clean
459 true, // canUndo
460 "append", // undoText
461 true, // canRedo
462 "insert", // redoText
463 false, // cleanChanged
464 true, // indexChanged
465 true, // undoChanged
466 true) // redoChanged
467
468 stack1->undo();
469 CHECK_STATE(stack1, // activeStack
470 true, // clean
471 false, // canUndo
472 "", // undoText
473 true, // canRedo
474 "append", // redoText
475 true, // cleanChanged
476 true, // indexChanged
477 true, // undoChanged
478 true) // redoChanged
479
480 stack1->undo();
481 CHECK_STATE(stack1, // activeStack
482 true, // clean
483 false, // canUndo
484 "", // undoText
485 true, // canRedo
486 "append", // redoText
487 false, // cleanChanged
488 false, // indexChanged
489 false, // undoChanged
490 false) // redoChanged
491
492 group.undo();
493 CHECK_STATE(stack1, // activeStack
494 true, // clean
495 false, // canUndo
496 "", // undoText
497 true, // canRedo
498 "append", // redoText
499 false, // cleanChanged
500 false, // indexChanged
501 false, // undoChanged
502 false) // redoChanged
503
504 group.redo();
505 CHECK_STATE(stack1, // activeStack
506 false, // clean
507 true, // canUndo
508 "append", // undoText
509 true, // canRedo
510 "insert", // redoText
511 true, // cleanChanged
512 true, // indexChanged
513 true, // undoChanged
514 true) // redoChanged
515
516 stack1->setActive(false);
517 CHECK_STATE(0, // activeStack
518 true, // clean
519 false, // canUndo
520 "", // undoText
521 false, // canRedo
522 "", // redoText
523 true, // cleanChanged
524 true, // indexChanged
525 true, // undoChanged
526 true) // redoChanged
527
528 QUndoStack *stack2 = new QUndoStack(&group);
529 CHECK_STATE(0, // activeStack
530 true, // clean
531 false, // canUndo
532 "", // undoText
533 false, // canRedo
534 "", // redoText
535 false, // cleanChanged
536 false, // indexChanged
537 false, // undoChanged
538 false) // redoChanged
539
540 stack2->setActive();
541 CHECK_STATE(stack2, // activeStack
542 true, // clean
543 false, // canUndo
544 "", // undoText
545 false, // canRedo
546 "", // redoText
547 true, // cleanChanged
548 true, // indexChanged
549 true, // undoChanged
550 true) // redoChanged
551
552 stack1->setActive();
553 CHECK_STATE(stack1, // activeStack
554 false, // clean
555 true, // canUndo
556 "append", // undoText
557 true, // canRedo
558 "insert", // redoText
559 true, // cleanChanged
560 true, // indexChanged
561 true, // undoChanged
562 true) // redoChanged
563
564 delete stack1;
565 CHECK_STATE(0, // activeStack
566 true, // clean
567 false, // canUndo
568 "", // undoText
569 false, // canRedo
570 "", // redoText
571 true, // cleanChanged
572 true, // indexChanged
573 true, // undoChanged
574 true) // redoChanged
575}
576
577void tst_QUndoGroup::addStackAndDie()
578{
579 // Test that QUndoStack doesn't keep a reference to QUndoGroup after the
580 // group is deleted.
581 QUndoStack *stack = new QUndoStack;
582 QUndoGroup *group = new QUndoGroup;
583 group->addStack(stack);
584 delete group;
585 stack->setActive(true);
586 delete stack;
587}
588
589void tst_QUndoGroup::commandTextFormat()
590{
591#if !QT_CONFIG(process)
592 QSKIP("No QProcess available");
593#else
594 QString binDir = QLibraryInfo::location(QLibraryInfo::BinariesPath);
595
596 if (QProcess::execute(command: binDir + "/lrelease -version") != 0)
597 QSKIP("lrelease is missing or broken");
598
599 const QString tsFile = QFINDTESTDATA("testdata/qundogroup.ts");
600 QVERIFY(!tsFile.isEmpty());
601 QFile::remove(fileName: "qundogroup.qm"); // Avoid confusion by strays.
602 QVERIFY(!QProcess::execute(binDir + "/lrelease -silent " + tsFile + " -qm qundogroup.qm"));
603
604 QTranslator translator;
605
606 QVERIFY(translator.load("qundogroup.qm"));
607 QFile::remove(fileName: "qundogroup.qm");
608 qApp->installTranslator(messageFile: &translator);
609
610 QUndoGroup group;
611 const QScopedPointer<QAction> undo_action(group.createUndoAction(parent: 0));
612 const QScopedPointer<QAction> redo_action(group.createRedoAction(parent: 0));
613
614 QCOMPARE(undo_action->text(), QString("Undo-default-text"));
615 QCOMPARE(redo_action->text(), QString("Redo-default-text"));
616
617 QUndoStack stack(&group);
618 stack.setActive();
619 QString str;
620
621 stack.push(cmd: new AppendCommand(&str, "foo"));
622 QCOMPARE(undo_action->text(), QString("undo-prefix append undo-suffix"));
623 QCOMPARE(redo_action->text(), QString("Redo-default-text"));
624
625 stack.push(cmd: new InsertCommand(&str, 0, "bar"));
626 stack.undo();
627 QCOMPARE(undo_action->text(), QString("undo-prefix append undo-suffix"));
628 QCOMPARE(redo_action->text(), QString("redo-prefix insert redo-suffix"));
629
630 stack.undo();
631 QCOMPARE(undo_action->text(), QString("Undo-default-text"));
632 QCOMPARE(redo_action->text(), QString("redo-prefix append redo-suffix"));
633
634 qApp->removeTranslator(messageFile: &translator);
635#endif
636}
637
638QTEST_MAIN(tst_QUndoGroup)
639
640#include "tst_qundogroup.moc"
641
642

source code of qtbase/tests/auto/widgets/util/qundogroup/tst_qundogroup.cpp