diff --git a/autotests/integration/globalshortcuts_test.cpp b/autotests/integration/globalshortcuts_test.cpp index 41b8d27251..3a6478772f 100644 --- a/autotests/integration/globalshortcuts_test.cpp +++ b/autotests/integration/globalshortcuts_test.cpp @@ -23,9 +23,12 @@ along with this program. If not, see . #include "platform.h" #include "screens.h" #include "shell_client.h" +#include "useractions.h" #include "wayland_server.h" #include "workspace.h" +#include +#include #include #include @@ -46,6 +49,7 @@ private Q_SLOTS: void testConsumedShift(); void testRepeatedTrigger(); + void testUserActionsMenu(); }; void GlobalShortcutsTest::initTestCase() @@ -67,12 +71,14 @@ void GlobalShortcutsTest::initTestCase() void GlobalShortcutsTest::init() { + QVERIFY(Test::setupWaylandConnection(s_socketName)); screens()->setCurrent(0); KWin::Cursor::setPos(QPoint(640, 512)); } void GlobalShortcutsTest::cleanup() { + Test::destroyWaylandConnection(); } void GlobalShortcutsTest::testConsumedShift() @@ -127,7 +133,6 @@ void GlobalShortcutsTest::testRepeatedTrigger() QVERIFY(triggeredSpy.wait()); // now release the key kwinApp()->platform()->keyboardKeyReleased(KEY_5, timestamp++); - QEXPECT_FAIL("", "BUG 369091", Continue); QVERIFY(!triggeredSpy.wait(500)); kwinApp()->platform()->keyboardKeyReleased(KEY_WAKEUP, timestamp++); @@ -137,5 +142,31 @@ void GlobalShortcutsTest::testRepeatedTrigger() kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTSHIFT, timestamp++); } +void GlobalShortcutsTest::testUserActionsMenu() +{ + // this test tries to trigger the user actions menu with Alt+F3 + // the problem here is that pressing F3 consumes modifiers as it's part of the + // Ctrl+alt+F3 keysym for vt switching. xkbcommon considers all modifiers as consumed + // which a transformation to any keysym would cause + // for more information see: + // https://bugs.freedesktop.org/show_bug.cgi?id=92818 + // https://github.com/xkbcommon/libxkbcommon/issues/17 + + // first create a window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + + quint32 timestamp = 0; + QVERIFY(!workspace()->userActionsMenu()->isShown()); + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + kwinApp()->platform()->keyboardKeyPressed(KEY_F3, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_F3, timestamp++); + QTRY_VERIFY(workspace()->userActionsMenu()->isShown()); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); +} + WAYLANDTEST_MAIN(GlobalShortcutsTest) #include "globalshortcuts_test.moc" diff --git a/autotests/integration/kwin_wayland_test.cpp b/autotests/integration/kwin_wayland_test.cpp index 95e7c330de..ccdd078d6c 100644 --- a/autotests/integration/kwin_wayland_test.cpp +++ b/autotests/integration/kwin_wayland_test.cpp @@ -46,6 +46,7 @@ static void readDisplay(int pipe); WaylandTestApplication::WaylandTestApplication(int &argc, char **argv) : Application(OperationModeXwayland, argc, argv) { + QStandardPaths::setTestModeEnabled(true); #ifdef KWIN_BUILD_ACTIVITIES setUseKActivities(false); #endif diff --git a/autotests/integration/modifier_only_shortcut_test.cpp b/autotests/integration/modifier_only_shortcut_test.cpp index f38318551c..68feb70ac1 100644 --- a/autotests/integration/modifier_only_shortcut_test.cpp +++ b/autotests/integration/modifier_only_shortcut_test.cpp @@ -49,6 +49,8 @@ private Q_SLOTS: void testTrigger_data(); void testTrigger(); void testCapsLock(); + void testGlobalShortcutsDisabled_data(); + void testGlobalShortcutsDisabled(); }; class Target : public QObject @@ -288,5 +290,74 @@ void ModifierOnlyShortcutTest::testCapsLock() QCOMPARE(triggeredSpy.count(), 1); } +void ModifierOnlyShortcutTest::testGlobalShortcutsDisabled_data() +{ + QTest::addColumn("metaConfig"); + QTest::addColumn("altConfig"); + QTest::addColumn("controlConfig"); + QTest::addColumn("shiftConfig"); + QTest::addColumn("modifier"); + + const QStringList trigger = QStringList{s_serviceName, s_path, s_serviceName, QStringLiteral("shortcut")}; + const QStringList e = QStringList(); + + QTest::newRow("leftMeta") << trigger << e << e << e << KEY_LEFTMETA; + QTest::newRow("rightMeta") << trigger << e << e << e << KEY_RIGHTMETA; + QTest::newRow("leftAlt") << e << trigger << e << e << KEY_LEFTALT; + QTest::newRow("rightAlt") << e << trigger << e << e << KEY_RIGHTALT; + QTest::newRow("leftControl") << e << e << trigger << e << KEY_LEFTCTRL; + QTest::newRow("rightControl") << e << e << trigger << e << KEY_RIGHTCTRL; + QTest::newRow("leftShift") << e << e << e << trigger << KEY_LEFTSHIFT; + QTest::newRow("rightShift") << e << e << e << trigger <config()->group("ModifierOnlyShortcuts"); + QFETCH(QStringList, metaConfig); + QFETCH(QStringList, altConfig); + QFETCH(QStringList, shiftConfig); + QFETCH(QStringList, controlConfig); + group.writeEntry("Meta", metaConfig); + group.writeEntry("Alt", altConfig); + group.writeEntry("Shift", shiftConfig); + group.writeEntry("Control", controlConfig); + group.sync(); + workspace()->slotReconfigure(); + + // trigger once to verify the shortcut works + quint32 timestamp = 1; + QFETCH(int, modifier); + QVERIFY(!workspace()->globalShortcutsDisabled()); + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); + triggeredSpy.clear(); + + // now disable global shortcuts + workspace()->disableGlobalShortcutsForClient(true); + QVERIFY(workspace()->globalShortcutsDisabled()); + // Should not get triggered + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 0); + triggeredSpy.clear(); + + // enable again + workspace()->disableGlobalShortcutsForClient(false); + QVERIFY(!workspace()->globalShortcutsDisabled()); + // should get triggered again + kwinApp()->platform()->keyboardKeyPressed(modifier, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(modifier, timestamp++); + QCOMPARE(triggeredSpy.count(), 1); +} + WAYLANDTEST_MAIN(ModifierOnlyShortcutTest) #include "modifier_only_shortcut_test.moc" diff --git a/keyboard_input.cpp b/keyboard_input.cpp index a2138a3f74..6c3a457042 100644 --- a/keyboard_input.cpp +++ b/keyboard_input.cpp @@ -310,7 +310,8 @@ void Xkb::updateKey(uint32_t key, InputRedirection::KeyboardKeyState state) } else { m_modOnlyShortcut.pressCount--; if (m_modOnlyShortcut.pressCount == 0 && - m_modifiers == Qt::NoModifier) { + m_modifiers == Qt::NoModifier && + !workspace()->globalShortcutsDisabled()) { if (m_modOnlyShortcut.modifier != Qt::NoModifier) { const auto list = options->modifierOnlyDBusShortcut(m_modOnlyShortcut.modifier); if (list.size() >= 4) { @@ -403,6 +404,24 @@ Qt::KeyboardModifiers Xkb::modifiersRelevantForGlobalShortcuts() const if (xkb_state_mod_index_is_active(m_state, m_metaModifier, XKB_STATE_MODS_EFFECTIVE) == 1) { mods |= Qt::MetaModifier; } + + // workaround xkbcommon limitation concerning consumed modifiers + // if a key could be turned into a keysym with a modifier xkbcommon + // considers the modifier as consumed even if not pressed + // e.g. alt+F3 considers alt as consumed as there is a keysym generated + // with ctrl+alt+F3 (vt switching) + // For more information see: + // https://bugs.freedesktop.org/show_bug.cgi?id=92818 + // https://github.com/xkbcommon/libxkbcommon/issues/17 + + // the workaround is to not consider the modifiers as consumed + // if they are not a currently + // this might have other side effects, though. The only proper way to + // handle this is through new API in xkbcommon which doesn't exist yet + if (m_consumedModifiers & ~m_modifiers) { + return mods; + } + return mods & ~m_consumedModifiers; } @@ -463,17 +482,25 @@ KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent) { } -KeyboardInputRedirection::~KeyboardInputRedirection() -{ - qDeleteAll(m_repeatTimers); - m_repeatTimers.clear(); -} +KeyboardInputRedirection::~KeyboardInputRedirection() = default; void KeyboardInputRedirection::init() { Q_ASSERT(!m_inited); m_inited = true; + // setup key repeat + m_keyRepeat.timer = new QTimer(this); + connect(m_keyRepeat.timer, &QTimer::timeout, this, + [this] { + if (waylandServer()->seat()->keyRepeatRate() != 0) { + m_keyRepeat.timer->setInterval(1000 / waylandServer()->seat()->keyRepeatRate()); + } + // TODO: better time + processKey(m_keyRepeat.key, InputRedirection::KeyboardKeyAutoRepeat, m_keyRepeat.time); + } + ); + connect(workspace(), &QObject::destroyed, this, [this] { m_inited = false; }); connect(waylandServer(), &QObject::destroyed, this, [this] { m_inited = false; }); connect(workspace(), &Workspace::clientActivated, this, @@ -615,26 +642,14 @@ void KeyboardInputRedirection::processKey(uint32_t key, InputRedirection::Keyboa device); if (state == InputRedirection::KeyboardKeyPressed) { if (m_xkb->shouldKeyRepeat(key) && waylandServer()->seat()->keyRepeatDelay() != 0) { - QTimer *timer = new QTimer; - timer->setInterval(waylandServer()->seat()->keyRepeatDelay()); - connect(timer, &QTimer::timeout, this, - [this, timer, time, key] { - const int delay = 1000 / waylandServer()->seat()->keyRepeatRate(); - if (timer->interval() != delay) { - timer->setInterval(delay); - } - // TODO: better time - processKey(key, InputRedirection::KeyboardKeyAutoRepeat, time); - } - ); - m_repeatTimers.insert(key, timer); - timer->start(); + m_keyRepeat.timer->setInterval(waylandServer()->seat()->keyRepeatDelay()); + m_keyRepeat.key = key; + m_keyRepeat.time = time; + m_keyRepeat.timer->start(); } } else if (state == InputRedirection::KeyboardKeyReleased) { - auto it = m_repeatTimers.find(key); - if (it != m_repeatTimers.end()) { - delete it.value(); - m_repeatTimers.erase(it); + if (key == m_keyRepeat.key) { + m_keyRepeat.timer->stop(); } } diff --git a/keyboard_input.h b/keyboard_input.h index 3e9808d937..dd43326556 100644 --- a/keyboard_input.h +++ b/keyboard_input.h @@ -140,8 +140,12 @@ private: InputRedirection *m_input; bool m_inited = false; QScopedPointer m_xkb; - QHash m_repeatTimers; QMetaObject::Connection m_activeClientSurfaceChangedConnection; + struct { + quint32 key = 0; + quint32 time = 0; + QTimer *timer = nullptr; + } m_keyRepeat; }; inline diff --git a/useractions.h b/useractions.h index 2a7927e9da..9f489346e7 100644 --- a/useractions.h +++ b/useractions.h @@ -21,6 +21,8 @@ along with this program. If not, see . #define KWIN_USERACTIONS_H #include "ui_shortcutdialog.h" +#include + // Qt #include #include @@ -52,7 +54,7 @@ class Client; * * @author Martin Gräßlin **/ -class UserActionsMenu : public QObject +class KWIN_EXPORT UserActionsMenu : public QObject { Q_OBJECT public: