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 <QDialog>
30#include <QMainWindow>
31#include <QtTest/QtTest>
32
33#include <qapplication.h>
34#include <qevent.h>
35#include <qaction.h>
36#include <qmenu.h>
37#include <qpa/qplatformtheme.h>
38#include <qpa/qplatformintegration.h>
39#include <private/qguiapplication_p.h>
40
41class tst_QAction : public QObject
42{
43 Q_OBJECT
44
45public:
46 tst_QAction();
47
48 void updateState(QActionEvent *e);
49
50private slots:
51 void init();
52 void cleanup();
53 void getSetCheck();
54 void setText_data();
55 void setText();
56 void setIconText_data() { setText_data(); }
57 void setIconText();
58 void setUnknownFont();
59 void actionEvent();
60 void setStandardKeys();
61 void alternateShortcuts();
62 void enabledVisibleInteraction();
63 void task200823_tooltip();
64 void task229128TriggeredSignalWithoutActiongroup();
65 void task229128TriggeredSignalWhenInActiongroup();
66 void repeat();
67 void setData();
68 void keysequence(); // QTBUG-53381
69 void disableShortcutsWithBlockedWidgets_data();
70 void disableShortcutsWithBlockedWidgets();
71 void shortcutFromKeyEvent(); // QTBUG-48325
72
73private:
74 QEvent::Type m_lastEventType;
75 const int m_keyboardScheme;
76 QAction *m_lastAction;
77};
78
79tst_QAction::tst_QAction()
80 : m_keyboardScheme(QGuiApplicationPrivate::platformTheme()->themeHint(hint: QPlatformTheme::KeyboardScheme).toInt())
81{
82}
83
84void tst_QAction::init()
85{
86 m_lastEventType = QEvent::None;
87 m_lastAction = nullptr;
88}
89
90void tst_QAction::cleanup()
91{
92 QVERIFY(QApplication::topLevelWidgets().isEmpty());
93}
94
95// Testing get/set functions
96void tst_QAction::getSetCheck()
97{
98 QAction obj1(nullptr);
99 // QActionGroup * QAction::actionGroup()
100 // void QAction::setActionGroup(QActionGroup *)
101 QActionGroup *var1 = new QActionGroup(nullptr);
102 obj1.setActionGroup(var1);
103 QCOMPARE(var1, obj1.actionGroup());
104 obj1.setActionGroup(nullptr);
105 QCOMPARE(obj1.actionGroup(), nullptr);
106 delete var1;
107
108 // QMenu * QAction::menu()
109 // void QAction::setMenu(QMenu *)
110 QMenu *var2 = new QMenu(nullptr);
111 obj1.setMenu(var2);
112 QCOMPARE(var2, obj1.menu());
113 obj1.setMenu(nullptr);
114 QCOMPARE(obj1.menu(), nullptr);
115 delete var2;
116
117 QCOMPARE(obj1.priority(), QAction::NormalPriority);
118 obj1.setPriority(QAction::LowPriority);
119 QCOMPARE(obj1.priority(), QAction::LowPriority);
120}
121
122class MyWidget : public QWidget
123{
124 Q_OBJECT
125public:
126 explicit MyWidget(tst_QAction *tst, QWidget *parent = nullptr) : QWidget(parent), m_test(tst)
127 { setWindowTitle(QTest::currentTestFunction()); }
128
129protected:
130 void actionEvent(QActionEvent *e) override { m_test->updateState(e); }
131
132private:
133 tst_QAction *m_test;
134};
135
136void tst_QAction::setText_data()
137{
138 QTest::addColumn<QString>(name: "text");
139 QTest::addColumn<QString>(name: "iconText");
140 QTest::addColumn<QString>(name: "textFromIconText");
141
142 //next we fill it with data
143 QTest::newRow(dataTag: "Normal") << "Action" << "Action" << "Action";
144 QTest::newRow(dataTag: "Ampersand") << "Search && Destroy" << "Search & Destroy" << "Search && Destroy";
145 QTest::newRow(dataTag: "Mnemonic and ellipsis") << "O&pen File ..." << "Open File" << "Open File";
146}
147
148void tst_QAction::setText()
149{
150 QFETCH(QString, text);
151
152 QAction action(nullptr);
153 action.setText(text);
154
155 QCOMPARE(action.text(), text);
156
157 QFETCH(QString, iconText);
158 QCOMPARE(action.iconText(), iconText);
159}
160
161void tst_QAction::setIconText()
162{
163 QFETCH(QString, iconText);
164
165 QAction action(nullptr);
166 action.setIconText(iconText);
167 QCOMPARE(action.iconText(), iconText);
168
169 QFETCH(QString, textFromIconText);
170 QCOMPARE(action.text(), textFromIconText);
171}
172
173void tst_QAction::setUnknownFont() // QTBUG-42728
174{
175 QAction action(nullptr);
176 QFont font("DoesNotExist", 11);
177 action.setFont(font);
178
179 QMenu menu;
180 menu.addAction(action: &action); // should not crash
181}
182
183void tst_QAction::updateState(QActionEvent *e)
184{
185 if (!e) {
186 m_lastEventType = QEvent::None;
187 m_lastAction = nullptr;
188 } else {
189 m_lastEventType = e->type();
190 m_lastAction = e->action();
191 }
192}
193
194void tst_QAction::actionEvent()
195{
196 QAction a(nullptr);
197 a.setText("action text");
198
199 // add action
200 MyWidget testWidget(this);
201 testWidget.show();
202 QApplication::setActiveWindow(&testWidget);
203 testWidget.addAction(action: &a);
204 qApp->processEvents();
205
206 QCOMPARE(m_lastEventType, QEvent::ActionAdded);
207 QCOMPARE(m_lastAction, &a);
208
209 // change action
210 a.setText("new action text");
211 qApp->processEvents();
212
213 QCOMPARE(m_lastEventType, QEvent::ActionChanged);
214 QCOMPARE(m_lastAction, &a);
215
216 // remove action
217 testWidget.removeAction(action: &a);
218 qApp->processEvents();
219
220 QCOMPARE(m_lastEventType, QEvent::ActionRemoved);
221 QCOMPARE(m_lastAction, &a);
222}
223
224//basic testing of standard keys
225void tst_QAction::setStandardKeys()
226{
227 QAction act(nullptr);
228 act.setShortcut(QKeySequence("CTRL+L"));
229 QList<QKeySequence> list;
230 act.setShortcuts(list);
231 act.setShortcuts(QKeySequence::Copy);
232 QCOMPARE(act.shortcut(), act.shortcuts().constFirst());
233
234 QList<QKeySequence> expected;
235 const QKeySequence ctrlC = QKeySequence(QStringLiteral("CTRL+C"));
236 const QKeySequence ctrlInsert = QKeySequence(QStringLiteral("CTRL+INSERT"));
237 switch (m_keyboardScheme) {
238 case QPlatformTheme::MacKeyboardScheme:
239 expected << ctrlC;
240 break;
241 case QPlatformTheme::WindowsKeyboardScheme:
242 expected << ctrlC << ctrlInsert;
243 break;
244 default: // X11
245 expected << ctrlC << ctrlInsert << QKeySequence(QStringLiteral("F16"));
246 break;
247 }
248
249 QCOMPARE(act.shortcuts(), expected);
250}
251
252
253void tst_QAction::alternateShortcuts()
254{
255 //test the alternate shortcuts (by adding more than 1 shortcut)
256
257 MyWidget testWidget(this);
258 testWidget.show();
259 QApplication::setActiveWindow(&testWidget);
260
261 {
262 QAction act(&testWidget);
263 testWidget.addAction(action: &act);
264 QList<QKeySequence> shlist = QList<QKeySequence>() << QKeySequence("CTRL+P") << QKeySequence("CTRL+A");
265 act.setShortcuts(shlist);
266
267 QSignalSpy spy(&act, &QAction::triggered);
268
269 act.setAutoRepeat(true);
270 QTest::keyClick(widget: &testWidget, key: Qt::Key_A, modifier: Qt::ControlModifier);
271 QCOMPARE(spy.count(), 1); //act should have been triggered
272
273 act.setAutoRepeat(false);
274 QTest::keyClick(widget: &testWidget, key: Qt::Key_A, modifier: Qt::ControlModifier);
275 QCOMPARE(spy.count(), 2); //act should have been triggered a 2nd time
276
277 //end of the scope of the action, it will be destroyed and removed from wid
278 //This action should also unregister its shortcuts
279 }
280
281
282 //this tests a crash (if the action did not unregister its alternate shortcuts)
283 QTest::keyClick(widget: &testWidget, key: Qt::Key_A, modifier: Qt::ControlModifier);
284}
285
286void tst_QAction::keysequence()
287{
288 MyWidget testWidget(this);
289 testWidget.show();
290 QApplication::setActiveWindow(&testWidget);
291
292 {
293 QAction act(&testWidget);
294 testWidget.addAction(action: &act);
295
296 QKeySequence ks(QKeySequence::SelectAll);
297
298 act.setShortcut(ks);
299
300 QSignalSpy spy(&act, &QAction::triggered);
301
302 act.setAutoRepeat(true);
303 QTest::keySequence(widget: &testWidget, keySequence: ks);
304 QCoreApplication::processEvents();
305 QCOMPARE(spy.count(), 1); // act should have been triggered
306
307 act.setAutoRepeat(false);
308 QTest::keySequence(widget: &testWidget, keySequence: ks);
309 QCoreApplication::processEvents();
310 QCOMPARE(spy.count(), 2); //act should have been triggered a 2nd time
311
312 // end of the scope of the action, it will be destroyed and removed from widget
313 // This action should also unregister its shortcuts
314 }
315
316 // this tests a crash (if the action did not unregister its alternate shortcuts)
317 QTest::keyClick(widget: &testWidget, key: Qt::Key_A, modifier: Qt::ControlModifier);
318}
319
320void tst_QAction::enabledVisibleInteraction()
321{
322 MyWidget testWidget(this);
323 testWidget.show();
324 QApplication::setActiveWindow(&testWidget);
325
326 QAction act(nullptr);
327 // check defaults
328 QVERIFY(act.isEnabled());
329 QVERIFY(act.isVisible());
330
331 // !visible => !enabled
332 act.setVisible(false);
333 QVERIFY(!act.isEnabled());
334 act.setVisible(true);
335 QVERIFY(act.isEnabled());
336 act.setEnabled(false);
337 QVERIFY(act.isVisible());
338
339 // check if shortcut is disabled if not visible
340 testWidget.addAction(action: &act);
341 act.setShortcut(QKeySequence("Ctrl+T"));
342 QSignalSpy spy(&act, SIGNAL(triggered()));
343 act.setEnabled(true);
344 act.setVisible(false);
345 QTest::keyClick(widget: &testWidget, key: Qt::Key_T, modifier: Qt::ControlModifier);
346 QCOMPARE(spy.count(), 0); //act is not visible, so don't trigger
347 act.setVisible(false);
348 act.setEnabled(true);
349 QTest::keyClick(widget: &testWidget, key: Qt::Key_T, modifier: Qt::ControlModifier);
350 QCOMPARE(spy.count(), 0); //act is not visible, so don't trigger
351 act.setVisible(true);
352 act.setEnabled(true);
353 QTest::keyClick(widget: &testWidget, key: Qt::Key_T, modifier: Qt::ControlModifier);
354 QCOMPARE(spy.count(), 1); //act is visible and enabled, so trigger
355}
356
357void tst_QAction::task200823_tooltip()
358{
359 const QScopedPointer<QAction> action(new QAction("foo", nullptr));
360 QString shortcut("ctrl+o");
361 action->setShortcut(shortcut);
362
363 // we want a non-standard tooltip that shows the shortcut
364 action->setToolTip(action->text() + QLatin1String(" (") + action->shortcut().toString() + QLatin1Char(')'));
365
366 QString ref = QLatin1String("foo (") + QKeySequence(shortcut).toString() + QLatin1Char(')');
367 QCOMPARE(action->toolTip(), ref);
368}
369
370void tst_QAction::task229128TriggeredSignalWithoutActiongroup()
371{
372 // test without a group
373 const QScopedPointer<QAction> actionWithoutGroup(new QAction("Test", nullptr));
374 QSignalSpy spyWithoutGroup(actionWithoutGroup.data(), QOverload<bool>::of(ptr: &QAction::triggered));
375 QCOMPARE(spyWithoutGroup.count(), 0);
376 actionWithoutGroup->trigger();
377 // signal should be emitted
378 QCOMPARE(spyWithoutGroup.count(), 1);
379
380 // it is now a checkable checked action
381 actionWithoutGroup->setCheckable(true);
382 actionWithoutGroup->setChecked(true);
383 spyWithoutGroup.clear();
384 QCOMPARE(spyWithoutGroup.count(), 0);
385 actionWithoutGroup->trigger();
386 // signal should be emitted
387 QCOMPARE(spyWithoutGroup.count(), 1);
388}
389
390void tst_QAction::task229128TriggeredSignalWhenInActiongroup()
391{
392 QActionGroup ag(nullptr);
393 QAction *action = new QAction("Test", &ag);
394 QAction *checkedAction = new QAction("Test 2", &ag);
395 ag.addAction(a: action);
396 action->setCheckable(true);
397 ag.addAction(a: checkedAction);
398 checkedAction->setCheckable(true);
399 checkedAction->setChecked(true);
400
401 QSignalSpy actionSpy(checkedAction, QOverload<bool>::of(ptr: &QAction::triggered));
402 QSignalSpy actionGroupSpy(&ag, QOverload<QAction*>::of(ptr: &QActionGroup::triggered));
403 QCOMPARE(actionGroupSpy.count(), 0);
404 QCOMPARE(actionSpy.count(), 0);
405 checkedAction->trigger();
406 // check that both the group and the action have emitted the signal
407 QCOMPARE(actionGroupSpy.count(), 1);
408 QCOMPARE(actionSpy.count(), 1);
409}
410
411void tst_QAction::repeat()
412{
413 if (QGuiApplication::platformName().startsWith(s: QLatin1String("wayland"), cs: Qt::CaseInsensitive))
414 QSKIP("Wayland: This fails. Figure out why.");
415
416 MyWidget testWidget(this);
417 testWidget.show();
418 QApplication::setActiveWindow(&testWidget);
419 QVERIFY(QTest::qWaitForWindowActive(&testWidget));
420
421 QAction act(&testWidget);
422 testWidget.addAction(action: &act);
423 act.setShortcut(QKeySequence(Qt::Key_F));
424 QSignalSpy spy(&act, SIGNAL(triggered()));
425
426 act.setAutoRepeat(true);
427 QTest::keyPress(widget: &testWidget, key: Qt::Key_F);
428 QTest::keyRelease(widget: &testWidget, key: Qt::Key_F);
429 QCOMPARE(spy.count(), 1);
430
431 spy.clear();
432 QTest::keyPress(widget: &testWidget, key: Qt::Key_F);
433 // repeat event
434 QTest::simulateEvent(widget: &testWidget, press: true, code: Qt::Key_F, modifier: Qt::NoModifier, text: QString("f"), repeat: true);
435 QTest::simulateEvent(widget: &testWidget, press: true, code: Qt::Key_F, modifier: Qt::NoModifier, text: QString("f"), repeat: true);
436 QTest::keyRelease(widget: &testWidget, key: Qt::Key_F);
437 QCOMPARE(spy.count(), 3);
438
439 spy.clear();
440 act.setAutoRepeat(false);
441 QTest::keyPress(widget: &testWidget, key: Qt::Key_F);
442 QTest::simulateEvent(widget: &testWidget, press: true, code: Qt::Key_F, modifier: Qt::NoModifier, text: QString("f"), repeat: true);
443 QTest::simulateEvent(widget: &testWidget, press: true, code: Qt::Key_F, modifier: Qt::NoModifier, text: QString("f"), repeat: true);
444 QTest::keyRelease(widget: &testWidget, key: Qt::Key_F);
445 QCOMPARE(spy.count(), 1);
446
447 spy.clear();
448 act.setAutoRepeat(true);
449 QTest::keyPress(widget: &testWidget, key: Qt::Key_F);
450 QTest::simulateEvent(widget: &testWidget, press: true, code: Qt::Key_F, modifier: Qt::NoModifier, text: QString("f"), repeat: true);
451 QTest::keyRelease(widget: &testWidget, key: Qt::Key_F);
452 QCOMPARE(spy.count(), 2);
453}
454
455void tst_QAction::setData() // QTBUG-62006
456{
457 QAction act(nullptr);
458 QSignalSpy spy(&act, &QAction::changed);
459 QCOMPARE(act.data(), QVariant());
460 QCOMPARE(spy.count(), 0);
461 act.setData(QVariant());
462 QCOMPARE(spy.count(), 0);
463
464 act.setData(-1);
465 QCOMPARE(spy.count(), 1);
466 act.setData(-1);
467 QCOMPARE(spy.count(), 1);
468}
469
470void tst_QAction::disableShortcutsWithBlockedWidgets_data()
471{
472 QTest::addColumn<Qt::ShortcutContext>(name: "shortcutContext");
473 QTest::addColumn<Qt::WindowModality>(name: "windowModality");
474
475 QTest::newRow(dataTag: "application modal dialog should block window shortcut.")
476 << Qt::WindowShortcut << Qt::ApplicationModal;
477
478 QTest::newRow(dataTag: "application modal dialog should block application shortcut.")
479 << Qt::ApplicationShortcut << Qt::ApplicationModal;
480
481 QTest::newRow(dataTag: "window modal dialog should block application shortcut.")
482 << Qt::ApplicationShortcut << Qt::WindowModal;
483
484 QTest::newRow(dataTag: "window modal dialog should block window shortcut.")
485 << Qt::WindowShortcut << Qt::WindowModal;
486}
487
488
489void tst_QAction::disableShortcutsWithBlockedWidgets()
490{
491 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::WindowActivation))
492 QSKIP("Window activation is not supported");
493
494 QMainWindow window;
495
496 QFETCH(Qt::ShortcutContext, shortcutContext);
497 QAction action(&window);
498 window.addAction(action: &action);
499 action.setShortcut(QKeySequence(Qt::Key_1));
500 action.setShortcutContext(shortcutContext);
501
502 window.show();
503 QVERIFY(QTest::qWaitForWindowExposed(&window));
504
505 QDialog dialog(&window);
506 QFETCH(Qt::WindowModality, windowModality);
507 dialog.setWindowModality(windowModality);
508
509 dialog.show();
510 QVERIFY(QTest::qWaitForWindowExposed(&dialog));
511
512 QApplication::setActiveWindow(&window);
513 QVERIFY(QTest::qWaitForWindowActive(&window));
514
515 QSignalSpy spy(&action, &QAction::triggered);
516 QTest::keyPress(widget: &window, key: Qt::Key_1);
517 QCOMPARE(spy.count(), 0);
518}
519
520class ShortcutOverrideWidget : public QWidget
521{
522public:
523 using QWidget::QWidget;
524 int shortcutOverrideCount = 0;
525protected:
526 bool event(QEvent *e) override
527 {
528 if (e->type() == QEvent::ShortcutOverride)
529 ++shortcutOverrideCount;
530 return QWidget::event(event: e);
531 }
532};
533
534// Test that a key press event sent with sendEvent() still gets handled as a possible
535// ShortcutOverride event first before passing it on as a normal KeyEvent.
536void tst_QAction::shortcutFromKeyEvent()
537{
538 if (QGuiApplication::platformName().startsWith(s: QLatin1String("wayland"), cs: Qt::CaseInsensitive))
539 QSKIP("Wayland: This fails. Figure out why.");
540
541 ShortcutOverrideWidget testWidget;
542 QAction action;
543 action.setShortcut(Qt::Key_1);
544 testWidget.addAction(action: &action);
545 testWidget.show();
546 QSignalSpy spy(&action, &QAction::triggered);
547 QVERIFY(spy.isValid());
548 QVERIFY(QTest::qWaitForWindowActive(&testWidget));
549 QCOMPARE(testWidget.shortcutOverrideCount, 0);
550
551 // Don't use the QTest::keyPress approach as this will take the
552 // shortcut route for us
553 QKeyEvent e(QEvent::KeyPress, Qt::Key_1, Qt::NoModifier);
554 QApplication::sendEvent(receiver: &testWidget, event: &e);
555 QCOMPARE(spy.count(), 1);
556 QCOMPARE(testWidget.shortcutOverrideCount, 1);
557}
558
559QTEST_MAIN(tst_QAction)
560#include "tst_qaction.moc"
561

source code of qtbase/tests/auto/widgets/kernel/qaction/tst_qaction.cpp