diff --git a/keyboard_input.h b/keyboard_input.h --- a/keyboard_input.h +++ b/keyboard_input.h @@ -38,6 +38,7 @@ typedef uint32_t xkb_mod_index_t; typedef uint32_t xkb_led_index_t; typedef uint32_t xkb_keysym_t; +typedef uint32_t xkb_layout_index_t; namespace KWin { @@ -73,6 +74,8 @@ bool shouldKeyRepeat(quint32 key) const; void switchToNextLayout(); + void switchToPreviousLayout(); + void switchToLayout(xkb_layout_index_t layout); enum class LED { NumLock = 1 << 0, @@ -96,14 +99,17 @@ return m_currentLayout; } QString layoutName() const; + QMap layoutNames() const; + quint32 numberOfLayouts() const; private: xkb_keymap *loadKeymapFromConfig(); xkb_keymap *loadDefaultKeymap(); void updateKeymap(xkb_keymap *keymap); void createKeymapFile(); void updateModifiers(); void updateConsumedModifiers(uint32_t key); + QString layoutName(xkb_layout_index_t layout) const; InputRedirection *m_input; xkb_context *m_context; xkb_keymap *m_keymap; diff --git a/keyboard_input.cpp b/keyboard_input.cpp --- a/keyboard_input.cpp +++ b/keyboard_input.cpp @@ -389,7 +389,22 @@ QString Xkb::layoutName() const { - return QString::fromLocal8Bit(xkb_keymap_layout_get_name(m_keymap, m_currentLayout)); + return layoutName(m_currentLayout); +} + +QString Xkb::layoutName(xkb_layout_index_t layout) const +{ + return QString::fromLocal8Bit(xkb_keymap_layout_get_name(m_keymap, layout)); +} + +QMap Xkb::layoutNames() const +{ + QMap layouts; + const auto size = xkb_keymap_num_layouts(m_keymap); + for (xkb_layout_index_t i = 0; i < size; i++) { + layouts.insert(i, layoutName(i)); + } + return layouts; } void Xkb::updateConsumedModifiers(uint32_t key) @@ -483,13 +498,41 @@ } const xkb_layout_index_t numLayouts = xkb_keymap_num_layouts(m_keymap); const xkb_layout_index_t nextLayout = (xkb_state_serialize_layout(m_state, XKB_STATE_LAYOUT_EFFECTIVE) + 1) % numLayouts; + switchToLayout(nextLayout); +} + +void Xkb::switchToPreviousLayout() +{ + if (!m_keymap || !m_state) { + return; + } + const xkb_layout_index_t previousLayout = m_currentLayout == 0 ? numberOfLayouts() - 1 : m_currentLayout -1; + switchToLayout(previousLayout); +} + +void Xkb::switchToLayout(xkb_layout_index_t layout) +{ + if (!m_keymap || !m_state) { + return; + } + if (layout >= numberOfLayouts()) { + return; + } const xkb_mod_mask_t depressed = xkb_state_serialize_mods(m_state, xkb_state_component(XKB_STATE_MODS_DEPRESSED)); const xkb_mod_mask_t latched = xkb_state_serialize_mods(m_state, xkb_state_component(XKB_STATE_MODS_LATCHED)); const xkb_mod_mask_t locked = xkb_state_serialize_mods(m_state, xkb_state_component(XKB_STATE_MODS_LOCKED)); - xkb_state_update_mask(m_state, depressed, latched, locked, 0, 0, nextLayout); + xkb_state_update_mask(m_state, depressed, latched, locked, 0, 0, layout); updateModifiers(); } +quint32 Xkb::numberOfLayouts() const +{ + if (!m_keymap) { + return 0; + } + return xkb_keymap_num_layouts(m_keymap); +} + KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent) : QObject(parent) , m_input(parent) @@ -558,6 +601,7 @@ m_modifiersChangedSpy = new ModifiersChangedSpy(m_input); m_input->installInputEventSpy(m_modifiersChangedSpy); m_keyboardLayout = new KeyboardLayout(m_xkb.data()); + m_keyboardLayout->setConfig(KSharedConfig::openConfig(QStringLiteral("kxkbrc"), KConfig::NoGlobals)); m_keyboardLayout->init(); m_input->installInputEventSpy(m_keyboardLayout); diff --git a/keyboard_layout.h b/keyboard_layout.h --- a/keyboard_layout.h +++ b/keyboard_layout.h @@ -22,8 +22,12 @@ #include "input_event_spy.h" #include + +#include typedef uint32_t xkb_layout_index_t; +class KStatusNotifierItem; + namespace KWin { class Xkb; @@ -35,6 +39,10 @@ explicit KeyboardLayout(Xkb *xkb); ~KeyboardLayout() override; + void setConfig(KSharedConfigPtr config) { + m_config = config; + } + void init(); void checkLayoutChange(); @@ -47,8 +55,15 @@ private: void notifyLayoutChange(); + void initNotifierItem(); + void switchToNextLayout(); + void switchToPreviousLayout(); + void updateNotifier(); + void reinitNotifierMenu(); Xkb *m_xkb; xkb_layout_index_t m_layout = 0; + KStatusNotifierItem *m_notifierItem; + KSharedConfigPtr m_config; }; } diff --git a/keyboard_layout.cpp b/keyboard_layout.cpp --- a/keyboard_layout.cpp +++ b/keyboard_layout.cpp @@ -22,20 +22,25 @@ #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) { } @@ -50,12 +55,7 @@ KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList({sequence})); KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList({sequence})); kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); - connect(switchKeyboardAction, &QAction::triggered, this, - [this] { - m_xkb->switchToNextLayout(); - checkLayoutChange(); - } - ); + connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout); QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/Layouts"), @@ -67,15 +67,81 @@ reconfigure(); } +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::reconfigure() { m_xkb->reconfigure(); + if (m_config) { + m_config->reparseConfiguration(); + } resetLayout(); } void KeyboardLayout::resetLayout() { m_layout = m_xkb->currentLayout(); + initNotifierItem(); + updateNotifier(); + reinitNotifierMenu(); } void KeyboardLayout::keyEvent(KeyEvent *event) @@ -93,6 +159,7 @@ } m_layout = layout; notifyLayoutChange(); + updateNotifier(); } void KeyboardLayout::notifyLayoutChange() @@ -109,4 +176,52 @@ QDBusConnection::sessionBus().asyncCall(msg); } +void KeyboardLayout::updateNotifier() +{ + if (!m_notifierItem) { + return; + } + m_notifierItem->setToolTipSubTitle(i18nd("xkeyboard-config", m_xkb->layoutName().toUtf8().constData())); + // TODO: update icon +} + +void KeyboardLayout::reinitNotifierMenu() +{ + if (!m_notifierItem) { + return; + } + const auto layouts = m_xkb->layoutNames(); + + QMenu *menu = new QMenu; + auto switchLayout = [this] (xkb_layout_index_t index) { + m_xkb->switchToLayout(index); + checkLayoutChange(); + }; + for (auto it = layouts.begin(); it != layouts.end(); it++) { + menu->addAction(i18nd("xkeyboard-config", it.value().toUtf8().constData()), std::bind(switchLayout, 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); +} + }