kwin/autotests/integration/keyboard_layout_test.cpp
Vlad Zahorodnii d2fb4147fc Move multi-purpose code in its own directory
Things such as Output, InputDevice and so on are made to be
multi-purpose. In order to make this separation more clear, this change
moves that code in the core directory. Some things still link to the
abstraction level above (kwin), they can be tackled in future refactors.
Ideally code in core/ should depend either on other code in core/ or
system libs.
2022-09-06 11:21:40 +03:00

562 lines
21 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2017 Martin Gräßlin <mgraesslin@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kwin_wayland_test.h"
#include "core/platform.h"
#include "keyboard_input.h"
#include "keyboard_layout.h"
#include "virtualdesktops.h"
#include "wayland_server.h"
#include "window.h"
#include "workspace.h"
#include <KConfigGroup>
#include <KGlobalAccel>
#include <KWayland/Client/surface.h>
#include <QAction>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusMessage>
#include <QDBusPendingCall>
#include <linux/input.h>
using namespace KWin;
using namespace KWayland::Client;
static const QString s_socketName = QStringLiteral("wayland_test_kwin_keyboard_laout-0");
class KeyboardLayoutTest : public QObject
{
Q_OBJECT
public:
KeyboardLayoutTest()
: layoutsReconfiguredSpy(this, &KeyboardLayoutTest::layoutListChanged)
, layoutChangedSpy(this, &KeyboardLayoutTest::layoutChanged)
{
QVERIFY(layoutsReconfiguredSpy.isValid());
QVERIFY(layoutChangedSpy.isValid());
QVERIFY(QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("layoutListChanged"), this, SIGNAL(layoutListChanged())));
QVERIFY(QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("layoutChanged"), this, SIGNAL(layoutChanged(uint))));
}
Q_SIGNALS:
void layoutChanged(uint index);
void layoutListChanged();
private Q_SLOTS:
void initTestCase();
void init();
void cleanup();
void testReconfigure();
void testChangeLayoutThroughDBus();
void testPerLayoutShortcut();
void testDBusServiceExport();
void testVirtualDesktopPolicy();
void testWindowPolicy();
void testApplicationPolicy();
void testNumLock();
private:
void reconfigureLayouts();
void resetLayouts();
auto changeLayout(uint index);
void callSession(const QString &method);
QSignalSpy layoutsReconfiguredSpy;
QSignalSpy layoutChangedSpy;
KConfigGroup layoutGroup;
};
void KeyboardLayoutTest::reconfigureLayouts()
{
// create DBus signal to reload
QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig"));
QVERIFY(QDBusConnection::sessionBus().send(message));
QVERIFY(layoutsReconfiguredSpy.wait(1000));
QCOMPARE(layoutsReconfiguredSpy.count(), 1);
layoutsReconfiguredSpy.clear();
}
void KeyboardLayoutTest::resetLayouts()
{
/* Switch Policy to destroy layouts from memory.
* On return to original Policy they should reload from disk.
*/
callSession(QStringLiteral("aboutToSaveSession"));
const QString policy = layoutGroup.readEntry("SwitchMode", "Global");
if (policy == QLatin1String("Global")) {
layoutGroup.writeEntry("SwitchMode", "Desktop");
} else {
layoutGroup.deleteEntry("SwitchMode");
}
reconfigureLayouts();
layoutGroup.writeEntry("SwitchMode", policy);
reconfigureLayouts();
callSession(QStringLiteral("loadSession"));
}
auto KeyboardLayoutTest::changeLayout(uint index)
{
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"),
QStringLiteral("/Layouts"),
QStringLiteral("org.kde.KeyboardLayouts"),
QStringLiteral("setLayout"));
msg << index;
return QDBusConnection::sessionBus().asyncCall(msg);
}
void KeyboardLayoutTest::callSession(const QString &method)
{
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
QStringLiteral("/Session"),
QStringLiteral("org.kde.KWin.Session"),
method);
msg << QLatin1String(); // session name
QVERIFY(QDBusConnection::sessionBus().call(msg).type() != QDBusMessage::ErrorMessage);
}
void KeyboardLayoutTest::initTestCase()
{
qRegisterMetaType<KWin::Window *>();
QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
QVERIFY(applicationStartedSpy.isValid());
kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
QVERIFY(waylandServer()->init(s_socketName));
kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));
kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));
layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
layoutGroup.deleteGroup();
kwinApp()->start();
QVERIFY(applicationStartedSpy.wait());
// don't get DBus signal on one-layout configuration
// QVERIFY(layoutsReconfiguredSpy.wait());
// QCOMPARE(layoutsReconfiguredSpy.count(), 1);
// layoutsReconfiguredSpy.clear();
}
void KeyboardLayoutTest::init()
{
QVERIFY(Test::setupWaylandConnection());
}
void KeyboardLayoutTest::cleanup()
{
Test::destroyWaylandConnection();
}
void KeyboardLayoutTest::testReconfigure()
{
// verifies that we can change the keymap
// default should be a keymap with only us layout
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 1u);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
QCOMPARE(xkb->numberOfLayouts(), 1);
QCOMPARE(xkb->layoutName(0), QStringLiteral("English (US)"));
// create a new keymap
KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout");
layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us"));
layoutGroup.sync();
reconfigureLayouts();
// now we should have two layouts
QCOMPARE(xkb->numberOfLayouts(), 2u);
// default layout is German
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
QCOMPARE(xkb->numberOfLayouts(), 2);
QCOMPARE(xkb->layoutName(0), QStringLiteral("German"));
QCOMPARE(xkb->layoutName(1), QStringLiteral("English (US)"));
}
void KeyboardLayoutTest::testChangeLayoutThroughDBus()
{
// this test verifies that the layout can be changed through DBus
// first configure layouts
enum Layout {
de,
us,
de_neo,
bad,
};
layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)"));
layoutGroup.sync();
reconfigureLayouts();
// now we should have three layouts
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 3u);
// default layout is German
xkb->switchToLayout(0);
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
// place garbage to layout entry
layoutGroup.writeEntry("LayoutDefaultFoo", "garbage");
// make sure the garbage is wiped out on saving
resetLayouts();
QVERIFY(!layoutGroup.hasKey("LayoutDefaultFoo"));
// now change through DBus to English
auto reply = changeLayout(Layout::us);
reply.waitForFinished();
QVERIFY(!reply.isError());
QCOMPARE(reply.reply().arguments().first().toBool(), true);
QVERIFY(layoutChangedSpy.wait());
QCOMPARE(layoutChangedSpy.count(), 1);
layoutChangedSpy.clear();
// layout should persist after reset
resetLayouts();
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
QVERIFY(layoutChangedSpy.wait());
QCOMPARE(layoutChangedSpy.count(), 1);
layoutChangedSpy.clear();
// switch to a layout which does not exist
reply = changeLayout(Layout::bad);
QVERIFY(!reply.isError());
QCOMPARE(reply.reply().arguments().first().toBool(), false);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
QVERIFY(!layoutChangedSpy.wait(1000));
// switch to another layout should work
reply = changeLayout(Layout::de);
QVERIFY(!reply.isError());
QCOMPARE(reply.reply().arguments().first().toBool(), true);
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
QVERIFY(layoutChangedSpy.wait(1000));
QCOMPARE(layoutChangedSpy.count(), 1);
// switching to same layout should also work
reply = changeLayout(Layout::de);
QVERIFY(!reply.isError());
QCOMPARE(reply.reply().arguments().first().toBool(), true);
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
QVERIFY(!layoutChangedSpy.wait(1000));
}
void KeyboardLayoutTest::testPerLayoutShortcut()
{
// this test verifies that per-layout global shortcuts are working correctly.
// first configure layouts
layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
layoutGroup.sync();
// and create the global shortcuts
const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher");
QAction *a = new QAction(this);
a->setObjectName(QStringLiteral("Switch keyboard layout to English (US)"));
a->setProperty("componentName", componentName);
KGlobalAccel::self()->setShortcut(a, QList<QKeySequence>{Qt::CTRL | Qt::ALT | Qt::Key_1}, KGlobalAccel::NoAutoloading);
delete a;
a = new QAction(this);
a->setObjectName(QStringLiteral("Switch keyboard layout to German"));
a->setProperty("componentName", componentName);
KGlobalAccel::self()->setShortcut(a, QList<QKeySequence>{Qt::CTRL | Qt::ALT | Qt::Key_2}, KGlobalAccel::NoAutoloading);
delete a;
// now we should have three layouts
auto xkb = input()->keyboard()->xkb();
reconfigureLayouts();
QCOMPARE(xkb->numberOfLayouts(), 3u);
// default layout is English
xkb->switchToLayout(0);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// now switch to English through the global shortcut
quint32 timestamp = 1;
Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
Test::keyboardKeyPressed(KEY_2, timestamp++);
QVERIFY(layoutChangedSpy.wait());
// now layout should be German
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
// release keys again
Test::keyboardKeyReleased(KEY_2, timestamp++);
// switch back to English
Test::keyboardKeyPressed(KEY_1, timestamp++);
QVERIFY(layoutChangedSpy.wait());
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// release keys again
Test::keyboardKeyReleased(KEY_1, timestamp++);
Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
}
void KeyboardLayoutTest::testDBusServiceExport()
{
// verifies that the dbus service is only exported if there are at least two layouts
// first configure layouts, with just one layout
layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
layoutGroup.sync();
reconfigureLayouts();
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 1u);
// default layout is English
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// with one layout we should not have the dbus interface
QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());
// reconfigure to two layouts
layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de"));
layoutGroup.sync();
reconfigureLayouts();
QCOMPARE(xkb->numberOfLayouts(), 2u);
QVERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());
// and back to one layout
layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
layoutGroup.sync();
reconfigureLayouts();
QCOMPARE(xkb->numberOfLayouts(), 1u);
QVERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value());
}
void KeyboardLayoutTest::testVirtualDesktopPolicy()
{
layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop"));
layoutGroup.sync();
reconfigureLayouts();
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 3u);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
VirtualDesktopManager::self()->setCount(4);
QCOMPARE(VirtualDesktopManager::self()->count(), 4u);
auto desktops = VirtualDesktopManager::self()->desktops();
QCOMPARE(desktops.count(), 4);
// give desktops different layouts
uint desktop, layout;
for (desktop = 0; desktop < VirtualDesktopManager::self()->count(); ++desktop) {
// switch to another virtual desktop
VirtualDesktopManager::self()->setCurrent(desktops.at(desktop));
QCOMPARE(desktops.at(desktop), VirtualDesktopManager::self()->currentDesktop());
// should be reset to English
QCOMPARE(xkb->currentLayout(), 0);
// change first desktop to German
layout = (desktop + 1) % xkb->numberOfLayouts();
changeLayout(layout).waitForFinished();
QCOMPARE(xkb->currentLayout(), layout);
}
// imitate app restart to test layouts saving feature
resetLayouts();
// check layout set on desktop switching as intended
for (--desktop;;) {
QCOMPARE(desktops.at(desktop), VirtualDesktopManager::self()->currentDesktop());
layout = (desktop + 1) % xkb->numberOfLayouts();
QCOMPARE(xkb->currentLayout(), layout);
if (--desktop >= VirtualDesktopManager::self()->count()) { // overflow
break;
}
VirtualDesktopManager::self()->setCurrent(desktops.at(desktop));
}
// remove virtual desktops
desktop = 0;
const KWin::VirtualDesktop *deletedDesktop = desktops.last();
VirtualDesktopManager::self()->setCount(1);
QCOMPARE(xkb->currentLayout(), layout = (desktop + 1) % xkb->numberOfLayouts());
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
// add another desktop
VirtualDesktopManager::self()->setCount(2);
// switching to it should result in going to default
desktops = VirtualDesktopManager::self()->desktops();
QCOMPARE(desktops.count(), 2);
QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop());
VirtualDesktopManager::self()->setCurrent(desktops.last());
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// check there are no more layouts left in config than the last actual non-default layouts number
QSignalSpy deletedDesktopSpy(deletedDesktop, &VirtualDesktop::aboutToBeDestroyed);
QVERIFY(deletedDesktopSpy.isValid());
QVERIFY(deletedDesktopSpy.wait());
resetLayouts();
QCOMPARE(layoutGroup.keyList().filter(QStringLiteral("LayoutDefault")).count(), 1);
}
void KeyboardLayoutTest::testWindowPolicy()
{
enum Layout {
us,
de,
de_neo,
bad,
};
layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
layoutGroup.writeEntry("SwitchMode", QStringLiteral("Window"));
layoutGroup.sync();
reconfigureLayouts();
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 3u);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// create a window
using namespace KWayland::Client;
std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
auto c1 = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
QVERIFY(c1);
// now switch layout
auto reply = changeLayout(Layout::de);
reply.waitForFinished();
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
// create a second window
std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 100), Qt::red);
QVERIFY(c2);
// this should have switched back to English
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// now change to another layout
reply = changeLayout(Layout::de_neo);
reply.waitForFinished();
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
// activate other window
workspace()->activateWindow(c1);
QCOMPARE(xkb->layoutName(), QStringLiteral("German"));
workspace()->activateWindow(c2);
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
}
void KeyboardLayoutTest::testApplicationPolicy()
{
enum Layout {
us,
de,
de_neo,
bad,
};
layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)"));
layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass"));
layoutGroup.sync();
reconfigureLayouts();
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 3u);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// create a window
using namespace KWayland::Client;
std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
shellSurface->set_app_id(QStringLiteral("org.kde.foo"));
auto c1 = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
QVERIFY(c1);
// create a second window
std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
shellSurface2->set_app_id(QStringLiteral("org.kde.foo"));
auto c2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 100), Qt::red);
QVERIFY(c2);
// now switch layout
layoutChangedSpy.clear();
changeLayout(Layout::de_neo);
QVERIFY(layoutChangedSpy.wait());
QCOMPARE(layoutChangedSpy.count(), 1);
layoutChangedSpy.clear();
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
resetLayouts();
// to trigger layout application for current client
workspace()->activateWindow(c1);
workspace()->activateWindow(c2);
QVERIFY(layoutChangedSpy.wait());
QCOMPARE(layoutChangedSpy.count(), 1);
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
// activate other window
workspace()->activateWindow(c1);
// it is the same application and should not switch the layout
QVERIFY(!layoutChangedSpy.wait(1000));
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
workspace()->activateWindow(c2);
QVERIFY(!layoutChangedSpy.wait(1000));
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
shellSurface2.reset();
surface2.reset();
QVERIFY(Test::waitForWindowDestroyed(c2));
QVERIFY(!layoutChangedSpy.wait(1000));
QCOMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)"));
resetLayouts();
QCOMPARE(layoutGroup.keyList().filter(QStringLiteral("LayoutDefault")).count(), 1);
}
void KeyboardLayoutTest::testNumLock()
{
qputenv("KWIN_FORCE_NUM_LOCK_EVALUATION", "1");
layoutGroup.writeEntry("LayoutList", QStringLiteral("us"));
layoutGroup.sync();
reconfigureLayouts();
auto xkb = input()->keyboard()->xkb();
QCOMPARE(xkb->numberOfLayouts(), 1u);
QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
// by default not set
QVERIFY(!xkb->leds().testFlag(LED::NumLock));
quint32 timestamp = 0;
Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
// now it should be on
QVERIFY(xkb->leds().testFlag(LED::NumLock));
// and back to off
Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
QVERIFY(!xkb->leds().testFlag(LED::NumLock));
// let's reconfigure to enable through config
auto group = InputConfig::self()->inputConfig()->group("Keyboard");
group.writeEntry("NumLock", 0);
group.sync();
xkb->reconfigure();
// now it should be on
QVERIFY(xkb->leds().testFlag(LED::NumLock));
// pressing should result in it being off
Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
QVERIFY(!xkb->leds().testFlag(LED::NumLock));
// pressing again should enable it
Test::keyboardKeyPressed(KEY_NUMLOCK, timestamp++);
Test::keyboardKeyReleased(KEY_NUMLOCK, timestamp++);
QVERIFY(xkb->leds().testFlag(LED::NumLock));
// now reconfigure to disable on load
group.writeEntry("NumLock", 1);
group.sync();
xkb->reconfigure();
QVERIFY(!xkb->leds().testFlag(LED::NumLock));
}
WAYLANDTEST_MAIN(KeyboardLayoutTest)
#include "keyboard_layout_test.moc"