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,16 @@ return m_currentLayout; } QString layoutName() const; + QMap layoutNames() 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,10 +498,30 @@ } 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 ? xkb_keymap_num_layouts(m_keymap) - 1 : m_currentLayout -1; + switchToLayout(previousLayout); +} + +void Xkb::switchToLayout(xkb_layout_index_t layout) +{ + if (!m_keymap || !m_state) { + return; + } + if (layout >= xkb_keymap_num_layouts(m_keymap)) { + 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(); } diff --git a/keyboard_layout.h b/keyboard_layout.h --- a/keyboard_layout.h +++ b/keyboard_layout.h @@ -24,6 +24,8 @@ #include typedef uint32_t xkb_layout_index_t; +class KStatusNotifierItem; + namespace KWin { class Xkb; @@ -47,8 +49,14 @@ 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; }; } diff --git a/keyboard_layout.cpp b/keyboard_layout.cpp --- a/keyboard_layout.cpp +++ b/keyboard_layout.cpp @@ -22,19 +22,24 @@ #include "input_event.h" #include "main.h" #include "platform.h" +#include "utils.h" #include +#include +#include #include #include #include #include +#include namespace KWin { KeyboardLayout::KeyboardLayout(Xkb *xkb) : QObject() , m_xkb(xkb) + , m_notifierItem(new KStatusNotifierItem(this)) { } @@ -49,23 +54,60 @@ 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"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig"), this, SLOT(reconfigure())); + initNotifierItem(); reconfigure(); } +void KeyboardLayout::initNotifierItem() +{ + 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(); @@ -75,6 +117,8 @@ void KeyboardLayout::resetLayout() { m_layout = m_xkb->currentLayout(); + updateNotifier(); + reinitNotifierMenu(); } void KeyboardLayout::keyEvent(KeyEvent *event) @@ -92,6 +136,7 @@ } m_layout = layout; notifyLayoutChange(); + updateNotifier(); } void KeyboardLayout::notifyLayoutChange() @@ -112,4 +157,46 @@ QDBusConnection::sessionBus().asyncCall(msg); } +void KeyboardLayout::updateNotifier() +{ + m_notifierItem->setToolTipSubTitle(m_xkb->layoutName()); + // TODO: update icon +} + +void KeyboardLayout::reinitNotifierMenu() +{ + 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(it.value(), 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); +} + }