Add a basic SNI for keyboard layout

Summary:
On X11 the SNI for keyboard layout is provided by the keyboard kded.
On Wayland that kded has no real access to the layouts and cannot
properly implement switching. Given that it's better to integrate the
SNI directly in KWin.

The implementation of the SNI is largly based on the existing SNI from
plasma-desktop/kcms/keyboard. The implementation so far supports:
 * Switching to next layout on toggle
 * Presenting all layouts in a context menu
 * Switching to a specific layout through the context menu
 * Opening the keyboard layout configuration module
 * scroll on SNI to switch layout
 * config option whether to show the SNI

Not yet supported are:
 * flags and/or short text for the layouts

The last point needs more explanation. On X11 the layout name is
something like "de" or "us". This can be directly mapped to a flag and
can be added as a short note.

Xkbcommon does not provide this information directly. Instead it provides
us the full name of the layout, e.g. "German" or "English (us)". There is
no way in the API to go from "German" to "de".

Instead we need to parse the evdev.xml file to gather all information
about layouts. This is already done in the keyboard kcm to configure
layouts. The implementation needs to be split out into a small helper
library.

Reviewers: #kwin, #plasma_on_wayland

Subscribers: plasma-devel, kwin

Tags: #plasma_on_wayland, #kwin

Differential Revision: https://phabricator.kde.org/D4220
This commit is contained in:
Martin Gräßlin 2017-01-20 07:15:27 +01:00
parent 1173f190bc
commit fe561c5c7d
4 changed files with 188 additions and 8 deletions

View file

@ -389,7 +389,22 @@ void Xkb::updateModifiers()
QString Xkb::layoutName() const 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_layout_index_t, QString> Xkb::layoutNames() const
{
QMap<xkb_layout_index_t, QString> 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) void Xkb::updateConsumedModifiers(uint32_t key)
@ -483,13 +498,41 @@ void Xkb::switchToNextLayout()
} }
const xkb_layout_index_t numLayouts = xkb_keymap_num_layouts(m_keymap); 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; 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 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 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)); 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(); updateModifiers();
} }
quint32 Xkb::numberOfLayouts() const
{
if (!m_keymap) {
return 0;
}
return xkb_keymap_num_layouts(m_keymap);
}
KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent) KeyboardInputRedirection::KeyboardInputRedirection(InputRedirection *parent)
: QObject(parent) : QObject(parent)
, m_input(parent) , m_input(parent)
@ -558,6 +601,7 @@ void KeyboardInputRedirection::init()
m_modifiersChangedSpy = new ModifiersChangedSpy(m_input); m_modifiersChangedSpy = new ModifiersChangedSpy(m_input);
m_input->installInputEventSpy(m_modifiersChangedSpy); m_input->installInputEventSpy(m_modifiersChangedSpy);
m_keyboardLayout = new KeyboardLayout(m_xkb.data()); m_keyboardLayout = new KeyboardLayout(m_xkb.data());
m_keyboardLayout->setConfig(KSharedConfig::openConfig(QStringLiteral("kxkbrc"), KConfig::NoGlobals));
m_keyboardLayout->init(); m_keyboardLayout->init();
m_input->installInputEventSpy(m_keyboardLayout); m_input->installInputEventSpy(m_keyboardLayout);

View file

@ -38,6 +38,7 @@ struct xkb_compose_state;
typedef uint32_t xkb_mod_index_t; typedef uint32_t xkb_mod_index_t;
typedef uint32_t xkb_led_index_t; typedef uint32_t xkb_led_index_t;
typedef uint32_t xkb_keysym_t; typedef uint32_t xkb_keysym_t;
typedef uint32_t xkb_layout_index_t;
namespace KWin namespace KWin
{ {
@ -73,6 +74,8 @@ public:
bool shouldKeyRepeat(quint32 key) const; bool shouldKeyRepeat(quint32 key) const;
void switchToNextLayout(); void switchToNextLayout();
void switchToPreviousLayout();
void switchToLayout(xkb_layout_index_t layout);
enum class LED { enum class LED {
NumLock = 1 << 0, NumLock = 1 << 0,
@ -96,6 +99,8 @@ public:
return m_currentLayout; return m_currentLayout;
} }
QString layoutName() const; QString layoutName() const;
QMap<xkb_layout_index_t, QString> layoutNames() const;
quint32 numberOfLayouts() const;
private: private:
xkb_keymap *loadKeymapFromConfig(); xkb_keymap *loadKeymapFromConfig();
@ -104,6 +109,7 @@ private:
void createKeymapFile(); void createKeymapFile();
void updateModifiers(); void updateModifiers();
void updateConsumedModifiers(uint32_t key); void updateConsumedModifiers(uint32_t key);
QString layoutName(xkb_layout_index_t layout) const;
InputRedirection *m_input; InputRedirection *m_input;
xkb_context *m_context; xkb_context *m_context;
xkb_keymap *m_keymap; xkb_keymap *m_keymap;

View file

@ -22,13 +22,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "input_event.h" #include "input_event.h"
#include "main.h" #include "main.h"
#include "platform.h" #include "platform.h"
#include "utils.h"
#include <KConfigGroup>
#include <KGlobalAccel> #include <KGlobalAccel>
#include <KLocalizedString> #include <KLocalizedString>
#include <KNotifications/KStatusNotifierItem>
#include <QAction> #include <QAction>
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusMessage> #include <QDBusMessage>
#include <QDBusPendingCall> #include <QDBusPendingCall>
#include <QMenu>
namespace KWin namespace KWin
{ {
@ -36,6 +40,7 @@ namespace KWin
KeyboardLayout::KeyboardLayout(Xkb *xkb) KeyboardLayout::KeyboardLayout(Xkb *xkb)
: QObject() : QObject()
, m_xkb(xkb) , m_xkb(xkb)
, m_notifierItem(nullptr)
{ {
} }
@ -50,12 +55,7 @@ void KeyboardLayout::init()
KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); KGlobalAccel::self()->setDefaultShortcut(switchKeyboardAction, QList<QKeySequence>({sequence}));
KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList<QKeySequence>({sequence})); KGlobalAccel::self()->setShortcut(switchKeyboardAction, QList<QKeySequence>({sequence}));
kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction); kwinApp()->platform()->setupActionForGlobalAccel(switchKeyboardAction);
connect(switchKeyboardAction, &QAction::triggered, this, connect(switchKeyboardAction, &QAction::triggered, this, &KeyboardLayout::switchToNextLayout);
[this] {
m_xkb->switchToNextLayout();
checkLayoutChange();
}
);
QDBusConnection::sessionBus().connect(QString(), QDBusConnection::sessionBus().connect(QString(),
QStringLiteral("/Layouts"), QStringLiteral("/Layouts"),
@ -67,15 +67,81 @@ void KeyboardLayout::init()
reconfigure(); 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() void KeyboardLayout::reconfigure()
{ {
m_xkb->reconfigure(); m_xkb->reconfigure();
if (m_config) {
m_config->reparseConfiguration();
}
resetLayout(); resetLayout();
} }
void KeyboardLayout::resetLayout() void KeyboardLayout::resetLayout()
{ {
m_layout = m_xkb->currentLayout(); m_layout = m_xkb->currentLayout();
initNotifierItem();
updateNotifier();
reinitNotifierMenu();
} }
void KeyboardLayout::keyEvent(KeyEvent *event) void KeyboardLayout::keyEvent(KeyEvent *event)
@ -93,6 +159,7 @@ void KeyboardLayout::checkLayoutChange()
} }
m_layout = layout; m_layout = layout;
notifyLayoutChange(); notifyLayoutChange();
updateNotifier();
} }
void KeyboardLayout::notifyLayoutChange() void KeyboardLayout::notifyLayoutChange()
@ -109,4 +176,52 @@ void KeyboardLayout::notifyLayoutChange()
QDBusConnection::sessionBus().asyncCall(msg); 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<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), p, &QProcess::deleteLater);
connect(p, static_cast<void (QProcess::*)(QProcess::ProcessError)>(&QProcess::error), this,
[p] (QProcess::ProcessError e) {
if (e == QProcess::FailedToStart) {
qCDebug(KWIN_CORE) << "Failed to start kcmshell5";
}
}
);
p->start();
}
);
m_notifierItem->setContextMenu(menu);
}
} }

View file

@ -22,8 +22,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "input_event_spy.h" #include "input_event_spy.h"
#include <QObject> #include <QObject>
#include <KSharedConfig>
typedef uint32_t xkb_layout_index_t; typedef uint32_t xkb_layout_index_t;
class KStatusNotifierItem;
namespace KWin namespace KWin
{ {
class Xkb; class Xkb;
@ -35,6 +39,10 @@ public:
explicit KeyboardLayout(Xkb *xkb); explicit KeyboardLayout(Xkb *xkb);
~KeyboardLayout() override; ~KeyboardLayout() override;
void setConfig(KSharedConfigPtr config) {
m_config = config;
}
void init(); void init();
void checkLayoutChange(); void checkLayoutChange();
@ -47,8 +55,15 @@ private Q_SLOTS:
private: private:
void notifyLayoutChange(); void notifyLayoutChange();
void initNotifierItem();
void switchToNextLayout();
void switchToPreviousLayout();
void updateNotifier();
void reinitNotifierMenu();
Xkb *m_xkb; Xkb *m_xkb;
xkb_layout_index_t m_layout = 0; xkb_layout_index_t m_layout = 0;
KStatusNotifierItem *m_notifierItem;
KSharedConfigPtr m_config;
}; };
} }