File indexing completed on 2024-05-05 17:35:46

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"