diff --git a/autotests/integration/keyboard_layout_test.cpp b/autotests/integration/keyboard_layout_test.cpp --- a/autotests/integration/keyboard_layout_test.cpp +++ b/autotests/integration/keyboard_layout_test.cpp @@ -59,6 +59,7 @@ void testDBusServiceExport(); void testVirtualDesktopPolicy(); void testWindowPolicy(); + void testApplicationPolicy(); private: void reconfigureLayouts(); @@ -394,5 +395,67 @@ QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); } +void KeyboardLayoutTest::testApplicationPolicy() +{ + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // now switch layout + auto changeLayout = [] (const QString &layoutName) { + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("setLayout")); + msg << layoutName; + return QDBusConnection::sessionBus().asyncCall(msg); + }; + LayoutChangedSignalWrapper wrapper; + QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged); + QVERIFY(layoutChangedSpy.isValid()); + auto reply = changeLayout(QStringLiteral("German")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + + // create a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // it is the same application and should not switch the layout + QVERIFY(!layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + // now change to another layout + reply = changeLayout(QStringLiteral("German (Neo 2)")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 2); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateClient(c1); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + workspace()->activateClient(c2); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + shellSurface2.reset(); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c2)); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); +} + WAYLANDTEST_MAIN(KeyboardLayoutTest) #include "keyboard_layout_test.moc" diff --git a/keyboard_layout_switching.h b/keyboard_layout_switching.h --- a/keyboard_layout_switching.h +++ b/keyboard_layout_switching.h @@ -112,6 +112,26 @@ QHash m_layouts; }; +class ApplicationPolicy : public Policy +{ + Q_OBJECT +public: + explicit ApplicationPolicy(Xkb *xkb, KeyboardLayout *layout); + ~ApplicationPolicy() override; + + QString name() const override { + return QStringLiteral("WinClass"); + } + +protected: + void clearCache() override; + void layoutChanged() override; + +private: + void clientActivated(AbstractClient *c); + QHash m_layouts; +}; + } } diff --git a/keyboard_layout_switching.cpp b/keyboard_layout_switching.cpp --- a/keyboard_layout_switching.cpp +++ b/keyboard_layout_switching.cpp @@ -60,6 +60,9 @@ if (policy.toLower() == QStringLiteral("window")) { return new WindowPolicy(xkb, layout); } + if (policy.toLower() == QStringLiteral("winclass")) { + return new ApplicationPolicy(xkb, layout); + } return new GlobalPolicy(xkb, layout); } @@ -182,5 +185,74 @@ } } +ApplicationPolicy::ApplicationPolicy(KWin::Xkb* xkb, KWin::KeyboardLayout* layout) + : Policy(xkb, layout) +{ + connect(workspace(), &Workspace::clientActivated, this, &ApplicationPolicy::clientActivated); +} + +ApplicationPolicy::~ApplicationPolicy() +{ +} + +void ApplicationPolicy::clientActivated(AbstractClient *c) +{ + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + quint32 layout = 0; + for (auto it = m_layouts.constBegin(); it != m_layouts.constEnd(); it++) { + if (AbstractClient::belongToSameApplication(c, it.key())) { + layout = it.value(); + break; + } + } + setLayout(layout); +} + +void ApplicationPolicy::clearCache() +{ + m_layouts.clear(); +} + +void ApplicationPolicy::layoutChanged() +{ + auto c = workspace()->activeClient(); + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + + auto it = m_layouts.find(c); + const auto l = layout(); + if (it == m_layouts.constEnd()) { + m_layouts.insert(c, l); + connect(c, &AbstractClient::windowClosed, this, + [this, c] { + m_layouts.remove(c); + } + ); + } else { + if (it.value() == l) { + return; + } + it.value() = l; + } + // update all layouts for the application + for (it = m_layouts.begin(); it != m_layouts.end(); it++) { + if (!AbstractClient::belongToSameApplication(it.key(), c)) { + continue; + } + it.value() = l; + } +} + } }