diff --git a/autotests/integration/keyboard_layout_test.cpp b/autotests/integration/keyboard_layout_test.cpp index 215c7180d..3a56ea16a 100644 --- a/autotests/integration/keyboard_layout_test.cpp +++ b/autotests/integration/keyboard_layout_test.cpp @@ -1,159 +1,188 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2017 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "keyboard_input.h" +#include "keyboard_layout.h" #include "platform.h" #include "wayland_server.h" #include #include #include #include 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 private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testReconfigure(); void testChangeLayoutThroughDBus(); private: void reconfigureLayouts(); }; void KeyboardLayoutTest::reconfigureLayouts() { // create DBus signal to reload QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig")); QDBusConnection::sessionBus().send(message); } void KeyboardLayoutTest::initTestCase() { QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); kwinApp()->setKxkbConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); waylandServer()->initWorkspace(); } void KeyboardLayoutTest::init() { } void KeyboardLayoutTest::cleanup() { } +class LayoutChangedSignalWrapper : public QObject +{ + Q_OBJECT +public: + LayoutChangedSignalWrapper() + : QObject() + { + QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.keyboard"), QStringLiteral("/Layouts"), QStringLiteral("org.kde.KeyboardLayouts"), QStringLiteral("currentLayoutChanged"), this, SIGNAL(layoutChanged(QString))); + } + +Q_SIGNALS: + void layoutChanged(const QString &name); +}; + 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)")); auto layouts = xkb->layoutNames(); QCOMPARE(layouts.size(), 1); QVERIFY(layouts.contains(0)); QCOMPARE(layouts[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 QTRY_COMPARE(xkb->numberOfLayouts(), 2u); // default layout is German QCOMPARE(xkb->layoutName(), QStringLiteral("German")); layouts = xkb->layoutNames(); QCOMPARE(layouts.size(), 2); QVERIFY(layouts.contains(0)); QVERIFY(layouts.contains(1)); QCOMPARE(layouts[0], QStringLiteral("German")); QCOMPARE(layouts[1], QStringLiteral("English (US)")); } void KeyboardLayoutTest::testChangeLayoutThroughDBus() { // this test verifies that the layout can be changed through DBus // first configure layouts KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)")); layoutGroup.sync(); reconfigureLayouts(); // now we should have two layouts auto xkb = input()->keyboard()->xkb(); QTRY_COMPARE(xkb->numberOfLayouts(), 3u); // default layout is German xkb->switchToLayout(0); QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + LayoutChangedSignalWrapper wrapper; + QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged); + QVERIFY(layoutChangedSpy.isValid()); + // now change through DBus to english 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); }; auto reply = changeLayout(QStringLiteral("English (US)")); reply.waitForFinished(); QVERIFY(!reply.isError()); QCOMPARE(reply.reply().arguments().first().toBool(), true); 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(QStringLiteral("French")); QVERIFY(!reply.isError()); QCOMPARE(reply.reply().arguments().first().toBool(), false); QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(!layoutChangedSpy.wait()); + QVERIFY(layoutChangedSpy.isEmpty()); // switch to another layout should work reply = changeLayout(QStringLiteral("German")); QVERIFY(!reply.isError()); QCOMPARE(reply.reply().arguments().first().toBool(), true); QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + layoutChangedSpy.clear(); // switching to same layout should also work reply = changeLayout(QStringLiteral("German")); QVERIFY(!reply.isError()); QCOMPARE(reply.reply().arguments().first().toBool(), true); QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + QVERIFY(!layoutChangedSpy.wait()); + QVERIFY(layoutChangedSpy.isEmpty()); } WAYLANDTEST_MAIN(KeyboardLayoutTest) #include "keyboard_layout_test.moc" diff --git a/keyboard_layout.cpp b/keyboard_layout.cpp index a1a5b1313..d63841f24 100644 --- a/keyboard_layout.cpp +++ b/keyboard_layout.cpp @@ -1,326 +1,328 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016, 2017 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "keyboard_layout.h" #include "keyboard_input.h" #include "input_event.h" #include "main.h" #include "platform.h" #include "utils.h" #include #include #include #include #include #include #include #include #include namespace KWin { KeyboardLayout::KeyboardLayout(Xkb *xkb) : QObject() , m_xkb(xkb) , m_notifierItem(nullptr) { } KeyboardLayout::~KeyboardLayout() = default; static QString translatedLayout(const QString &layout) { return i18nd("xkeyboard-config", layout.toUtf8().constData()); } void KeyboardLayout::init() { QAction *switchKeyboardAction = new QAction(this); switchKeyboardAction->setObjectName(QStringLiteral("Switch to Next Keyboard Layout")); switchKeyboardAction->setProperty("componentName", QStringLiteral("KDE Keyboard Layout Switcher")); const QKeySequence sequence = QKeySequence(Qt::ALT+Qt::CTRL+Qt::Key_K); KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList({sequence})); KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList({sequence})); kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout); QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig"), this, SLOT(reconfigure())); reconfigure(); initDBusInterface(); } void KeyboardLayout::initDBusInterface() { auto dbusInterface = new KeyboardLayoutDBusInterface(m_xkb, this); connect(this, &KeyboardLayout::layoutChanged, dbusInterface, [this, dbusInterface] { emit dbusInterface->currentLayoutChanged(m_xkb->layoutName()); } ); // TODO: the signal might be emitted even if the list didn't change connect(this, &KeyboardLayout::layoutsReconfigured, dbusInterface, &KeyboardLayoutDBusInterface::layoutListChanged); } void KeyboardLayout::initNotifierItem() { bool showNotifier = true; bool showSingle = false; if (m_config) { const auto config = m_config->group(QStringLiteral("Layout")); showNotifier = config.readEntry("ShowLayoutIndicator", true); showSingle = config.readEntry("ShowSingle", false); } const bool shouldShow = showNotifier && (showSingle || m_xkb->numberOfLayouts() > 1); if (shouldShow) { if (m_notifierItem) { return; } } else { delete m_notifierItem; m_notifierItem = nullptr; return; } m_notifierItem = new KStatusNotifierItem(this); m_notifierItem->setCategory(KStatusNotifierItem::Hardware); m_notifierItem->setStatus(KStatusNotifierItem::Active); m_notifierItem->setToolTipTitle(i18nc("tooltip title", "Keyboard Layout")); m_notifierItem->setTitle(i18nc("tooltip title", "Keyboard Layout")); m_notifierItem->setToolTipIconByName(QStringLiteral("preferences-desktop-keyboard")); m_notifierItem->setStandardActionsEnabled(false); // TODO: proper icon m_notifierItem->setIconByName(QStringLiteral("preferences-desktop-keyboard")); connect(m_notifierItem, &KStatusNotifierItem::activateRequested, this, &KeyboardLayout::switchToNextLayout); connect(m_notifierItem, &KStatusNotifierItem::scrollRequested, this, [this] (int delta, Qt::Orientation orientation) { if (orientation == Qt::Horizontal) { return; } if (delta > 0) { switchToNextLayout(); } else { switchToPreviousLayout(); } } ); m_notifierItem->setStatus(KStatusNotifierItem::Active); } void KeyboardLayout::switchToNextLayout() { m_xkb->switchToNextLayout(); checkLayoutChange(); } void KeyboardLayout::switchToPreviousLayout() { m_xkb->switchToPreviousLayout(); checkLayoutChange(); } void KeyboardLayout::switchToLayout(xkb_layout_index_t index) { m_xkb->switchToLayout(index); checkLayoutChange(); } void KeyboardLayout::reconfigure() { if (m_config) { m_config->reparseConfiguration(); } m_xkb->reconfigure(); resetLayout(); } void KeyboardLayout::resetLayout() { m_layout = m_xkb->currentLayout(); initNotifierItem(); updateNotifier(); reinitNotifierMenu(); loadShortcuts(); emit layoutsReconfigured(); } void KeyboardLayout::loadShortcuts() { qDeleteAll(m_layoutShortcuts); m_layoutShortcuts.clear(); const auto layouts = m_xkb->layoutNames(); const QString componentName = QStringLiteral("KDE Keyboard Layout Switcher"); for (auto it = layouts.begin(); it != layouts.end(); it++) { // layout name is translated in the action name in keyboard kcm! const QString action = QStringLiteral("Switch keyboard layout to %1").arg(translatedLayout(it.value())); const auto shortcuts = KGlobalAccel::self()->globalShortcut(componentName, action); if (shortcuts.isEmpty()) { continue; } QAction *a = new QAction(this); a->setObjectName(action); a->setProperty("componentName", componentName); connect(a, &QAction::triggered, this, std::bind(&KeyboardLayout::switchToLayout, this, it.key())); KGlobalAccel::self()->setShortcut(a, shortcuts, KGlobalAccel::Autoloading); m_layoutShortcuts << a; } } void KeyboardLayout::keyEvent(KeyEvent *event) { if (!event->isAutoRepeat()) { checkLayoutChange(); } } void KeyboardLayout::checkLayoutChange() { const auto layout = m_xkb->currentLayout(); if (m_layout == layout) { return; } m_layout = layout; notifyLayoutChange(); updateNotifier(); emit layoutChanged(); } void KeyboardLayout::notifyLayoutChange() { // notify OSD service about the new layout QDBusMessage msg = QDBusMessage::createMethodCall( QStringLiteral("org.kde.plasmashell"), QStringLiteral("/org/kde/osdService"), QStringLiteral("org.kde.osdService"), QStringLiteral("kbdLayoutChanged")); msg << translatedLayout(m_xkb->layoutName()); QDBusConnection::sessionBus().asyncCall(msg); } void KeyboardLayout::updateNotifier() { if (!m_notifierItem) { return; } m_notifierItem->setToolTipSubTitle(translatedLayout(m_xkb->layoutName())); // TODO: update icon } void KeyboardLayout::reinitNotifierMenu() { if (!m_notifierItem) { return; } const auto layouts = m_xkb->layoutNames(); QMenu *menu = new QMenu; for (auto it = layouts.begin(); it != layouts.end(); it++) { menu->addAction(translatedLayout(it.value()), std::bind(&KeyboardLayout::switchToLayout, this, it.key())); } menu->addSeparator(); menu->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Layouts..."), this, [this] { // TODO: introduce helper function to start kcmshell5 QProcess *p = new Process(this); p->setArguments(QStringList{QStringLiteral("--args=--tab=layouts"), QStringLiteral("kcm_keyboard")}); p->setProcessEnvironment(kwinApp()->processStartupEnvironment()); p->setProgram(QStringLiteral("kcmshell5")); connect(p, static_cast(&QProcess::finished), p, &QProcess::deleteLater); connect(p, static_cast(&QProcess::error), this, [p] (QProcess::ProcessError e) { if (e == QProcess::FailedToStart) { qCDebug(KWIN_CORE) << "Failed to start kcmshell5"; } } ); p->start(); } ); m_notifierItem->setContextMenu(menu); } static const QString s_keyboardService = QStringLiteral("org.kde.keyboard"); static const QString s_keyboardObject = QStringLiteral("/Layouts"); -KeyboardLayoutDBusInterface::KeyboardLayoutDBusInterface(Xkb *xkb, QObject *parent) +KeyboardLayoutDBusInterface::KeyboardLayoutDBusInterface(Xkb *xkb, KeyboardLayout *parent) : QObject(parent) , m_xkb(xkb) + , m_keyboardLayout(parent) { QDBusConnection::sessionBus().registerService(s_keyboardService); QDBusConnection::sessionBus().registerObject(s_keyboardObject, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals); } KeyboardLayoutDBusInterface::~KeyboardLayoutDBusInterface() { QDBusConnection::sessionBus().unregisterService(s_keyboardService); } bool KeyboardLayoutDBusInterface::setLayout(const QString &layout) { const auto layouts = m_xkb->layoutNames(); auto it = layouts.begin(); for (; it !=layouts.end(); it++) { if (it.value() == layout) { break; } } if (it == layouts.end()) { return false; } m_xkb->switchToLayout(it.key()); + m_keyboardLayout->checkLayoutChange(); return true; } QString KeyboardLayoutDBusInterface::getCurrentLayout() { return m_xkb->layoutName(); } QStringList KeyboardLayoutDBusInterface::getLayoutsList() { const auto layouts = m_xkb->layoutNames(); QStringList ret; for (auto it = layouts.begin(); it != layouts.end(); it++) { ret << it.value(); } return ret; } QString KeyboardLayoutDBusInterface::getLayoutDisplayName(const QString &layout) { return translatedLayout(layout); } } diff --git a/keyboard_layout.h b/keyboard_layout.h index 8cadb43d5..a0b2f374b 100644 --- a/keyboard_layout.h +++ b/keyboard_layout.h @@ -1,104 +1,105 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016, 2017 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_KEYBOARD_LAYOUT_H #define KWIN_KEYBOARD_LAYOUT_H #include "input_event_spy.h" #include #include #include typedef uint32_t xkb_layout_index_t; class KStatusNotifierItem; class QAction; namespace KWin { class Xkb; class KeyboardLayout : public QObject, public InputEventSpy { Q_OBJECT public: explicit KeyboardLayout(Xkb *xkb); ~KeyboardLayout() override; void setConfig(KSharedConfigPtr config) { m_config = config; } void init(); void checkLayoutChange(); void resetLayout(); void keyEvent(KeyEvent *event) override; Q_SIGNALS: void layoutChanged(); void layoutsReconfigured(); private Q_SLOTS: void reconfigure(); private: void initDBusInterface(); void notifyLayoutChange(); void initNotifierItem(); void switchToNextLayout(); void switchToPreviousLayout(); void switchToLayout(xkb_layout_index_t index); void updateNotifier(); void reinitNotifierMenu(); void loadShortcuts(); Xkb *m_xkb; xkb_layout_index_t m_layout = 0; KStatusNotifierItem *m_notifierItem; KSharedConfigPtr m_config; QVector m_layoutShortcuts; }; class KeyboardLayoutDBusInterface : public QObject { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.KeyboardLayouts") public: - explicit KeyboardLayoutDBusInterface(Xkb *xkb, QObject *parent); + explicit KeyboardLayoutDBusInterface(Xkb *xkb, KeyboardLayout *parent); ~KeyboardLayoutDBusInterface() override; public Q_SLOTS: bool setLayout(const QString &layout); QString getCurrentLayout(); QStringList getLayoutsList(); QString getLayoutDisplayName(const QString &layout); Q_SIGNALS: void currentLayoutChanged(QString layout); void layoutListChanged(); private: Xkb *m_xkb; + KeyboardLayout *m_keyboardLayout; }; } #endif