File indexing completed on 2025-03-23 13:47:56
0001 /* 0002 KWin - the KDE window manager 0003 This file is part of the KDE project. 0004 0005 SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org> 0006 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 #include "kwin_wayland_test.h" 0010 0011 #include "core/outputbackend.h" 0012 #include "cursor.h" 0013 #include "input.h" 0014 #include "internalwindow.h" 0015 #include "keyboard_input.h" 0016 #include "useractions.h" 0017 #include "wayland/keyboard_interface.h" 0018 #include "wayland/seat_interface.h" 0019 #include "wayland_server.h" 0020 #include "workspace.h" 0021 #include "x11window.h" 0022 #include "xkb.h" 0023 0024 #include <KWayland/Client/surface.h> 0025 0026 #include <KGlobalAccel> 0027 0028 #include <QAction> 0029 0030 #include <linux/input.h> 0031 #include <netwm.h> 0032 #include <xcb/xcb_icccm.h> 0033 0034 using namespace KWin; 0035 0036 static const QString s_socketName = QStringLiteral("wayland_test_kwin_globalshortcuts-0"); 0037 0038 class GlobalShortcutsTest : public QObject 0039 { 0040 Q_OBJECT 0041 private Q_SLOTS: 0042 void initTestCase(); 0043 void init(); 0044 void cleanup(); 0045 0046 void testNonLatinLayout_data(); 0047 void testNonLatinLayout(); 0048 void testConsumedShift(); 0049 void testRepeatedTrigger(); 0050 void testUserActionsMenu(); 0051 void testMetaShiftW(); 0052 void testComponseKey(); 0053 void testX11WindowShortcut(); 0054 void testWaylandWindowShortcut(); 0055 void testSetupWindowShortcut(); 0056 }; 0057 0058 void GlobalShortcutsTest::initTestCase() 0059 { 0060 qRegisterMetaType<KWin::Window *>(); 0061 qRegisterMetaType<KWin::InternalWindow *>(); 0062 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); 0063 QVERIFY(waylandServer()->init(s_socketName)); 0064 QMetaObject::invokeMethod(kwinApp()->outputBackend(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(QVector<QRect>, QVector<QRect>() << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024))); 0065 0066 kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); 0067 qputenv("KWIN_XKB_DEFAULT_KEYMAP", "1"); 0068 qputenv("XKB_DEFAULT_RULES", "evdev"); 0069 qputenv("XKB_DEFAULT_LAYOUT", "us,ru"); 0070 0071 kwinApp()->start(); 0072 QVERIFY(applicationStartedSpy.wait()); 0073 } 0074 0075 void GlobalShortcutsTest::init() 0076 { 0077 QVERIFY(Test::setupWaylandConnection()); 0078 workspace()->setActiveOutput(QPoint(640, 512)); 0079 KWin::Cursors::self()->mouse()->setPos(QPoint(640, 512)); 0080 0081 auto xkb = input()->keyboard()->xkb(); 0082 xkb->switchToLayout(0); 0083 } 0084 0085 void GlobalShortcutsTest::cleanup() 0086 { 0087 Test::destroyWaylandConnection(); 0088 } 0089 0090 Q_DECLARE_METATYPE(Qt::Modifier) 0091 0092 void GlobalShortcutsTest::testNonLatinLayout_data() 0093 { 0094 QTest::addColumn<int>("modifierKey"); 0095 QTest::addColumn<Qt::Modifier>("qtModifier"); 0096 QTest::addColumn<int>("key"); 0097 QTest::addColumn<Qt::Key>("qtKey"); 0098 0099 // KEY_W is "ц" in the RU layout and "w" in the US layout 0100 // KEY_GRAVE is "ё" in the RU layout and "`" in the US layout 0101 // TAB_KEY is the same both in the US and RU layout 0102 0103 QTest::newRow("Left Ctrl + Tab") << KEY_LEFTCTRL << Qt::CTRL << KEY_TAB << Qt::Key_Tab; 0104 QTest::newRow("Left Ctrl + W") << KEY_LEFTCTRL << Qt::CTRL << KEY_W << Qt::Key_W; 0105 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0106 QTest::newRow("Left Ctrl + `") << KEY_LEFTCTRL << Qt::CTRL << KEY_GRAVE << Qt::Key_QuoteLeft; 0107 #endif 0108 0109 QTest::newRow("Left Alt + Tab") << KEY_LEFTALT << Qt::ALT << KEY_TAB << Qt::Key_Tab; 0110 QTest::newRow("Left Alt + W") << KEY_LEFTALT << Qt::ALT << KEY_W << Qt::Key_W; 0111 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0112 QTest::newRow("Left Alt + `") << KEY_LEFTALT << Qt::ALT << KEY_GRAVE << Qt::Key_QuoteLeft; 0113 #endif 0114 0115 QTest::newRow("Left Shift + Tab") << KEY_LEFTSHIFT << Qt::SHIFT << KEY_TAB << Qt::Key_Tab; 0116 0117 QTest::newRow("Left Meta + Tab") << KEY_LEFTMETA << Qt::META << KEY_TAB << Qt::Key_Tab; 0118 QTest::newRow("Left Meta + W") << KEY_LEFTMETA << Qt::META << KEY_W << Qt::Key_W; 0119 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0120 QTest::newRow("Left Meta + `") << KEY_LEFTMETA << Qt::META << KEY_GRAVE << Qt::Key_QuoteLeft; 0121 #endif 0122 } 0123 0124 void GlobalShortcutsTest::testNonLatinLayout() 0125 { 0126 // Shortcuts on non-Latin layouts should still work, see BUG 375518 0127 auto xkb = input()->keyboard()->xkb(); 0128 xkb->switchToLayout(1); 0129 QCOMPARE(xkb->layoutName(), QStringLiteral("Russian")); 0130 0131 QFETCH(int, modifierKey); 0132 QFETCH(Qt::Modifier, qtModifier); 0133 QFETCH(int, key); 0134 QFETCH(Qt::Key, qtKey); 0135 0136 const QKeySequence seq(qtModifier + qtKey); 0137 0138 std::unique_ptr<QAction> action(new QAction(nullptr)); 0139 action->setProperty("componentName", QStringLiteral("kwin")); 0140 action->setObjectName("globalshortcuts-test-non-latin-layout"); 0141 0142 QSignalSpy triggeredSpy(action.get(), &QAction::triggered); 0143 0144 KGlobalAccel::self()->stealShortcutSystemwide(seq); 0145 KGlobalAccel::self()->setShortcut(action.get(), {seq}, KGlobalAccel::NoAutoloading); 0146 0147 quint32 timestamp = 0; 0148 Test::keyboardKeyPressed(modifierKey, timestamp++); 0149 QCOMPARE(input()->keyboardModifiers(), qtModifier); 0150 Test::keyboardKeyPressed(key, timestamp++); 0151 0152 Test::keyboardKeyReleased(key, timestamp++); 0153 Test::keyboardKeyReleased(modifierKey, timestamp++); 0154 0155 QTRY_COMPARE_WITH_TIMEOUT(triggeredSpy.count(), 1, 100); 0156 } 0157 0158 void GlobalShortcutsTest::testConsumedShift() 0159 { 0160 // this test verifies that a shortcut with a consumed shift modifier triggers 0161 // create the action 0162 std::unique_ptr<QAction> action(new QAction(nullptr)); 0163 action->setProperty("componentName", QStringLiteral("kwin")); 0164 action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); 0165 QSignalSpy triggeredSpy(action.get(), &QAction::triggered); 0166 KGlobalAccel::self()->setShortcut(action.get(), QList<QKeySequence>{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); 0167 0168 // press shift+5 0169 quint32 timestamp = 0; 0170 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0171 QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); 0172 Test::keyboardKeyPressed(KEY_5, timestamp++); 0173 QTRY_COMPARE(triggeredSpy.count(), 1); 0174 Test::keyboardKeyReleased(KEY_5, timestamp++); 0175 0176 // release shift 0177 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0178 } 0179 0180 void GlobalShortcutsTest::testRepeatedTrigger() 0181 { 0182 // this test verifies that holding a key, triggers repeated global shortcut 0183 // in addition pressing another key should stop triggering the shortcut 0184 0185 std::unique_ptr<QAction> action(new QAction(nullptr)); 0186 action->setProperty("componentName", QStringLiteral("kwin")); 0187 action->setObjectName(QStringLiteral("globalshortcuts-test-consumed-shift")); 0188 QSignalSpy triggeredSpy(action.get(), &QAction::triggered); 0189 KGlobalAccel::self()->setShortcut(action.get(), QList<QKeySequence>{Qt::Key_Percent}, KGlobalAccel::NoAutoloading); 0190 0191 // we need to configure the key repeat first. It is only enabled on libinput 0192 waylandServer()->seat()->keyboard()->setRepeatInfo(25, 300); 0193 0194 // press shift+5 0195 quint32 timestamp = 0; 0196 Test::keyboardKeyPressed(KEY_WAKEUP, timestamp++); 0197 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0198 QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier); 0199 Test::keyboardKeyPressed(KEY_5, timestamp++); 0200 QTRY_COMPARE(triggeredSpy.count(), 1); 0201 // and should repeat 0202 QVERIFY(triggeredSpy.wait()); 0203 QVERIFY(triggeredSpy.wait()); 0204 // now release the key 0205 Test::keyboardKeyReleased(KEY_5, timestamp++); 0206 QVERIFY(!triggeredSpy.wait(50)); 0207 0208 Test::keyboardKeyReleased(KEY_WAKEUP, timestamp++); 0209 QVERIFY(!triggeredSpy.wait(50)); 0210 0211 // release shift 0212 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0213 } 0214 0215 void GlobalShortcutsTest::testUserActionsMenu() 0216 { 0217 // this test tries to trigger the user actions menu with Alt+F3 0218 // the problem here is that pressing F3 consumes modifiers as it's part of the 0219 // Ctrl+alt+F3 keysym for vt switching. xkbcommon considers all modifiers as consumed 0220 // which a transformation to any keysym would cause 0221 // for more information see: 0222 // https://bugs.freedesktop.org/show_bug.cgi?id=92818 0223 // https://github.com/xkbcommon/libxkbcommon/issues/17 0224 0225 // first create a window 0226 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface()); 0227 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get())); 0228 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); 0229 QVERIFY(window); 0230 QVERIFY(window->isActive()); 0231 0232 quint32 timestamp = 0; 0233 QVERIFY(!workspace()->userActionsMenu()->isShown()); 0234 Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++); 0235 Test::keyboardKeyPressed(KEY_F3, timestamp++); 0236 Test::keyboardKeyReleased(KEY_F3, timestamp++); 0237 QTRY_VERIFY(workspace()->userActionsMenu()->isShown()); 0238 Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++); 0239 } 0240 0241 void GlobalShortcutsTest::testMetaShiftW() 0242 { 0243 // BUG 370341 0244 std::unique_ptr<QAction> action(new QAction(nullptr)); 0245 action->setProperty("componentName", QStringLiteral("kwin")); 0246 action->setObjectName(QStringLiteral("globalshortcuts-test-meta-shift-w")); 0247 QSignalSpy triggeredSpy(action.get(), &QAction::triggered); 0248 KGlobalAccel::self()->setShortcut(action.get(), QList<QKeySequence>{Qt::META | Qt::SHIFT | Qt::Key_W}, KGlobalAccel::NoAutoloading); 0249 0250 // press meta+shift+w 0251 quint32 timestamp = 0; 0252 Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); 0253 QCOMPARE(input()->keyboardModifiers(), Qt::MetaModifier); 0254 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0255 QCOMPARE(input()->keyboardModifiers(), Qt::ShiftModifier | Qt::MetaModifier); 0256 Test::keyboardKeyPressed(KEY_W, timestamp++); 0257 QTRY_COMPARE(triggeredSpy.count(), 1); 0258 Test::keyboardKeyReleased(KEY_W, timestamp++); 0259 0260 // release meta+shift 0261 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0262 Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); 0263 } 0264 0265 void GlobalShortcutsTest::testComponseKey() 0266 { 0267 // BUG 390110 0268 std::unique_ptr<QAction> action(new QAction(nullptr)); 0269 action->setProperty("componentName", QStringLiteral("kwin")); 0270 action->setObjectName(QStringLiteral("globalshortcuts-accent")); 0271 QSignalSpy triggeredSpy(action.get(), &QAction::triggered); 0272 KGlobalAccel::self()->setShortcut(action.get(), QList<QKeySequence>{Qt::NoModifier}, KGlobalAccel::NoAutoloading); 0273 0274 // press & release ` 0275 quint32 timestamp = 0; 0276 Test::keyboardKeyPressed(KEY_RESERVED, timestamp++); 0277 Test::keyboardKeyReleased(KEY_RESERVED, timestamp++); 0278 0279 QTRY_COMPARE(triggeredSpy.count(), 0); 0280 } 0281 0282 struct XcbConnectionDeleter 0283 { 0284 void operator()(xcb_connection_t *pointer) 0285 { 0286 xcb_disconnect(pointer); 0287 } 0288 }; 0289 0290 void GlobalShortcutsTest::testX11WindowShortcut() 0291 { 0292 #ifdef NO_XWAYLAND 0293 QSKIP("x11 test, unnecessary without xwayland"); 0294 #endif 0295 // create an X11 window 0296 std::unique_ptr<xcb_connection_t, XcbConnectionDeleter> c(xcb_connect(nullptr, nullptr)); 0297 QVERIFY(!xcb_connection_has_error(c.get())); 0298 xcb_window_t windowId = xcb_generate_id(c.get()); 0299 const QRect windowGeometry = QRect(0, 0, 10, 20); 0300 const uint32_t values[] = { 0301 XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW}; 0302 xcb_create_window(c.get(), XCB_COPY_FROM_PARENT, windowId, rootWindow(), 0303 windowGeometry.x(), 0304 windowGeometry.y(), 0305 windowGeometry.width(), 0306 windowGeometry.height(), 0307 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_EVENT_MASK, values); 0308 xcb_size_hints_t hints; 0309 memset(&hints, 0, sizeof(hints)); 0310 xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); 0311 xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); 0312 xcb_icccm_set_wm_normal_hints(c.get(), windowId, &hints); 0313 NETWinInfo info(c.get(), windowId, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); 0314 info.setWindowType(NET::Normal); 0315 xcb_map_window(c.get(), windowId); 0316 xcb_flush(c.get()); 0317 0318 QSignalSpy windowCreatedSpy(workspace(), &Workspace::windowAdded); 0319 QVERIFY(windowCreatedSpy.wait()); 0320 X11Window *window = windowCreatedSpy.last().first().value<X11Window *>(); 0321 QVERIFY(window); 0322 0323 QCOMPARE(workspace()->activeWindow(), window); 0324 QVERIFY(window->isActive()); 0325 QCOMPARE(window->shortcut(), QKeySequence()); 0326 const QKeySequence seq(Qt::META | Qt::SHIFT | Qt::Key_Y); 0327 QVERIFY(workspace()->shortcutAvailable(seq)); 0328 window->setShortcut(seq.toString()); 0329 QCOMPARE(window->shortcut(), seq); 0330 QVERIFY(!workspace()->shortcutAvailable(seq)); 0331 QCOMPARE(window->caption(), QStringLiteral(" {Meta+Shift+Y}")); 0332 0333 // it's delayed 0334 QCoreApplication::processEvents(); 0335 0336 workspace()->activateWindow(nullptr); 0337 QVERIFY(!workspace()->activeWindow()); 0338 QVERIFY(!window->isActive()); 0339 0340 // now let's trigger the shortcut 0341 quint32 timestamp = 0; 0342 Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); 0343 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0344 Test::keyboardKeyPressed(KEY_Y, timestamp++); 0345 QTRY_COMPARE(workspace()->activeWindow(), window); 0346 Test::keyboardKeyReleased(KEY_Y, timestamp++); 0347 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0348 Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); 0349 0350 // destroy window again 0351 QSignalSpy windowClosedSpy(window, &X11Window::windowClosed); 0352 xcb_unmap_window(c.get(), windowId); 0353 xcb_destroy_window(c.get(), windowId); 0354 xcb_flush(c.get()); 0355 QVERIFY(windowClosedSpy.wait()); 0356 } 0357 0358 void GlobalShortcutsTest::testWaylandWindowShortcut() 0359 { 0360 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface()); 0361 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get())); 0362 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); 0363 0364 QCOMPARE(workspace()->activeWindow(), window); 0365 QVERIFY(window->isActive()); 0366 QCOMPARE(window->shortcut(), QKeySequence()); 0367 const QKeySequence seq(Qt::META | Qt::SHIFT | Qt::Key_Y); 0368 QVERIFY(workspace()->shortcutAvailable(seq)); 0369 window->setShortcut(seq.toString()); 0370 QCOMPARE(window->shortcut(), seq); 0371 QVERIFY(!workspace()->shortcutAvailable(seq)); 0372 QCOMPARE(window->caption(), QStringLiteral(" {Meta+Shift+Y}")); 0373 0374 workspace()->activateWindow(nullptr); 0375 QVERIFY(!workspace()->activeWindow()); 0376 QVERIFY(!window->isActive()); 0377 0378 // now let's trigger the shortcut 0379 quint32 timestamp = 0; 0380 Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); 0381 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0382 Test::keyboardKeyPressed(KEY_Y, timestamp++); 0383 QTRY_COMPARE(workspace()->activeWindow(), window); 0384 Test::keyboardKeyReleased(KEY_Y, timestamp++); 0385 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0386 Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); 0387 0388 shellSurface.reset(); 0389 surface.reset(); 0390 QVERIFY(Test::waitForWindowDestroyed(window)); 0391 QTRY_VERIFY_WITH_TIMEOUT(workspace()->shortcutAvailable(seq), 500); // we need the try since KGlobalAccelPrivate::unregister is async 0392 } 0393 0394 void GlobalShortcutsTest::testSetupWindowShortcut() 0395 { 0396 // QTBUG-62102 0397 0398 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface()); 0399 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get())); 0400 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); 0401 0402 QCOMPARE(workspace()->activeWindow(), window); 0403 QVERIFY(window->isActive()); 0404 QCOMPARE(window->shortcut(), QKeySequence()); 0405 0406 QSignalSpy shortcutDialogAddedSpy(workspace(), &Workspace::internalWindowAdded); 0407 workspace()->slotSetupWindowShortcut(); 0408 QTRY_COMPARE(shortcutDialogAddedSpy.count(), 1); 0409 auto dialog = shortcutDialogAddedSpy.first().first().value<InternalWindow *>(); 0410 QVERIFY(dialog); 0411 QVERIFY(dialog->isInternal()); 0412 auto sequenceEdit = workspace()->shortcutDialog()->findChild<QKeySequenceEdit *>(); 0413 QVERIFY(sequenceEdit); 0414 0415 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0416 // the QKeySequenceEdit field does not get focus, we need to pass it focus manually 0417 QEXPECT_FAIL("", "Edit does not have focus", Continue); 0418 QVERIFY(sequenceEdit->hasFocus()); 0419 sequenceEdit->setFocus(); 0420 #endif 0421 QTRY_VERIFY(sequenceEdit->hasFocus()); 0422 0423 quint32 timestamp = 0; 0424 Test::keyboardKeyPressed(KEY_LEFTMETA, timestamp++); 0425 Test::keyboardKeyPressed(KEY_LEFTSHIFT, timestamp++); 0426 Test::keyboardKeyPressed(KEY_Y, timestamp++); 0427 Test::keyboardKeyReleased(KEY_Y, timestamp++); 0428 Test::keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); 0429 Test::keyboardKeyReleased(KEY_LEFTMETA, timestamp++); 0430 0431 // the sequence gets accepted after one second, so wait a bit longer 0432 QTest::qWait(2000); 0433 // now send in enter 0434 Test::keyboardKeyPressed(KEY_ENTER, timestamp++); 0435 Test::keyboardKeyReleased(KEY_ENTER, timestamp++); 0436 QTRY_COMPARE(window->shortcut(), QKeySequence(Qt::META | Qt::SHIFT | Qt::Key_Y)); 0437 } 0438 0439 WAYLANDTEST_MAIN(GlobalShortcutsTest) 0440 #include "globalshortcuts_test.moc"