Xwayland: Allow users to optionally let Xwayland eavesdrop on certain modes

It's somewhat popular for voice communication applications to support
Push-to-Talk. This means that the process itself expects to get all of
the system input. This behaviour albeit sound does not work on Wayland
systems.

This commit adds an option to let legacy X11 applications that assume
they will be getting all information to do so until these apps are
properly ported to the XDP GlobalShortcuts.
This commit is contained in:
Aleix Pol 2022-01-27 20:14:53 +01:00 committed by Aleix Pol Gonzalez
parent 245859637f
commit fc2447071e
21 changed files with 429 additions and 8 deletions

View file

@ -238,12 +238,13 @@ void InputEventFilter::passToWaylandServer(QKeyEvent *event)
}
KWaylandServer::SeatInterface *seat = waylandServer()->seat();
const int keyCode = event->nativeScanCode();
switch (event->type()) {
case QEvent::KeyPress:
seat->notifyKeyboardKey(event->nativeScanCode(), KWaylandServer::KeyboardKeyState::Pressed);
seat->notifyKeyboardKey(keyCode, KWaylandServer::KeyboardKeyState::Pressed);
break;
case QEvent::KeyRelease:
seat->notifyKeyboardKey(event->nativeScanCode(), KWaylandServer::KeyboardKeyState::Released);
seat->notifyKeyboardKey(keyCode, KWaylandServer::KeyboardKeyState::Released);
break;
default:
break;

View file

@ -615,7 +615,8 @@ void InputMethod::setPreeditString(uint32_t serial, const QString &text, const Q
void InputMethod::key(quint32 /*serial*/, quint32 /*time*/, quint32 keyCode, bool pressed)
{
waylandServer()->seat()->notifyKeyboardKey(keyCode, pressed ? KWaylandServer::KeyboardKeyState::Pressed : KWaylandServer::KeyboardKeyState::Released);
waylandServer()->seat()->notifyKeyboardKey(keyCode,
pressed ? KWaylandServer::KeyboardKeyState::Pressed : KWaylandServer::KeyboardKeyState::Released);
}
void InputMethod::modifiers(quint32 serial, quint32 mods_depressed, quint32 mods_latched, quint32 mods_locked, quint32 group)

View file

@ -10,6 +10,7 @@ add_subdirectory(kwinscripts)
add_subdirectory(kwindesktop)
add_subdirectory(kwineffects)
add_subdirectory(kwinvirtualkeyboard)
add_subdirectory(kwinxwayland)
if (KWIN_BUILD_TABBOX)
add_subdirectory(kwintabbox)

View file

@ -0,0 +1,25 @@
#SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalzez <aleixpol@kde.org>
#SPDX-License-Identifier: BSD-3-Clause
add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwinxwayland\")
kcmutils_generate_module_data(
kcm_kwinxwayland_PART_SRCS
MODULE_DATA_HEADER kwinxwaylanddata.h
MODULE_DATA_CLASS_NAME KWinXwaylandData
SETTINGS_HEADERS kwinxwaylandsettings.h
SETTINGS_CLASSES KWinXwaylandSettings
)
kconfig_add_kcfg_files(kcm_kwinxwayland_PART_SRCS kwinxwaylandsettings.kcfgc GENERATE_MOC)
kcoreaddons_add_plugin(kcm_kwinxwayland SOURCES kcmkwinxwayland.cpp ${kcm_kwinxwayland_PART_SRCS} INSTALL_NAMESPACE plasma/kcms/systemsettings)
target_include_directories(kcm_kwinxwayland PRIVATE ${CMAKE_SOURCE_DIR})
kcmutils_generate_desktop_file(kcm_kwinxwayland)
target_link_libraries(kcm_kwinxwayland
KF5::I18n
KF5::KCMUtils
KF5::QuickAddons
Wayland::Client
)
kpackage_install_package(package kcm_kwinxwayland kcms)

View file

@ -0,0 +1,2 @@
#! /usr/bin/env bash
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/kcm_kwinxwayland.pot

View file

@ -0,0 +1,15 @@
{
"Categories": "Qt;KDE;",
"KPlugin": {
"BugReportUrl": "https://bugs.kde.org/enter_bug.cgi?product=systemsettings&component=kcm_kwinxwayland",
"Description": "Select which keys will be globally available to legacy X11 apps",
"Icon": "xorg",
"Name": "Legacy X11 App Support"
},
"X-KDE-Keywords": "xwayland,global,keys,forward",
"X-KDE-OnlyShowOnQtPlatforms": [
"wayland"
],
"X-KDE-ParentApp": "kcontrol",
"X-KDE-System-Settings-Parent-Category": "applications"
}

View file

@ -0,0 +1,38 @@
/*
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kcmkwinxwayland.h"
#include <KAboutData>
#include <KApplicationTrader>
#include <KConfigGroup>
#include <KDesktopFile>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QKeySequence>
#include <kwinxwaylanddata.h>
K_PLUGIN_FACTORY_WITH_JSON(KcmXwaylandFactory, "kcm_kwinxwayland.json", registerPlugin<KcmXwayland>(); registerPlugin<KWinXwaylandData>();)
KcmXwayland::KcmXwayland(QObject *parent, const QVariantList &args)
: KQuickAddons::ManagedConfigModule(parent)
, m_data(new KWinXwaylandData(this))
, m_settings(new KWinXwaylandSettings(m_data))
{
registerSettings(m_settings);
qmlRegisterAnonymousType<KWinXwaylandSettings>("org.kde.kwin.kwinxwaylandsettings", 1);
setAboutData(new KAboutData(QStringLiteral("kcm_kwinxwayland"),
i18n("Legacy X11 App Support"),
QStringLiteral("1.0"),
i18n("Allow legacy X11 apps to read keystrokes typed in other apps"),
KAboutLicense::GPL));
}
KcmXwayland::~KcmXwayland() = default;
#include "kcmkwinxwayland.moc"

View file

@ -0,0 +1,36 @@
/*
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <KQuickAddons/ManagedConfigModule>
#include <KService>
#include <QAbstractListModel>
#include <kwinxwaylandsettings.h>
class KWinXwaylandData;
class KcmXwayland : public KQuickAddons::ManagedConfigModule
{
Q_OBJECT
Q_PROPERTY(KWinXwaylandSettings *settings READ settings CONSTANT)
public:
explicit KcmXwayland(QObject *parent = nullptr, const QVariantList &list = QVariantList());
~KcmXwayland() override;
KWinXwaylandSettings *settings() const
{
return m_settings;
}
private:
void refresh();
KWinXwaylandData *const m_data;
KWinXwaylandSettings *const m_settings;
};

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<kcfgfile name="kwinrc"/>
<group name="Xwayland">
<entry name="XwaylandEavesdrops" type="Enum">
<choices>
<choice name="None"/>
<choice name="Modifiers"/>
<choice name="Combinations"/>
<choice name="All"/>
</choices>
<default>None</default>
</entry>
</group>
</kcfg>

View file

@ -0,0 +1,8 @@
File=kwinxwaylandsettings.kcfg
ClassName=KWinXwaylandSettings
Mutators=true
DefaultValueGetters=true
GenerateProperties=true
ParentInConstructor=true
Notifiers=true
GlobalEnums=true

View file

@ -0,0 +1,70 @@
/*
SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.1
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3 as QQC2
import org.kde.kirigami 2.6 as Kirigami
import org.kde.kcm 1.3 as KCM
import org.kde.kwin.kwinxwaylandsettings 1.0
import org.kde.kquickcontrols 2.0
KCM.SimpleKCM {
id: root
KCM.ConfigModule.buttons: KCM.ConfigModule.Default | KCM.ConfigModule.Apply
KCM.ConfigModule.quickHelp: i18n("This module lets configure which keyboard events are forwarded to X11 apps regardless of their focus.")
KCM.SettingStateBinding {
configObject: kcm.settings
settingName: "Xwayland"
}
implicitWidth: Kirigami.Units.gridUnit * 48
implicitHeight: Kirigami.Units.gridUnit * 33
QQC2.ButtonGroup {
buttons: column.children
exclusive: true
checkedButton: buttons[kcm.settings.xwaylandEavesdrops]
onCheckedButtonChanged: {
let idx = -1
for (const x in buttons) {
if (buttons[x] === checkedButton) {
idx = x
}
}
kcm.settings.xwaylandEavesdrops = idx
}
}
ColumnLayout {
id: column
QQC2.Label {
Layout.fillWidth: true
text: i18n("Allow legacy X11 apps to read keystrokes typed in all apps:")
}
QQC2.RadioButton {
text: i18n("Never")
}
QQC2.RadioButton {
text: i18n("Only Meta, Control, Alt, and Shift keys")
}
QQC2.RadioButton {
text: i18n("All keys, but only while Meta, Ctrl, Alt, or Shift keys are pressed")
}
QQC2.RadioButton {
id: always
text: i18n("Always")
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Warning
text: i18n("Note that using this setting will reduce system security to that of the X11 session by permitting malicious software to steal passwords and spy on the text that you type. Make sure you understand and accept this risk.")
visible: always.checked
}
}
}

View file

@ -352,5 +352,15 @@
<entry name="XwaylandMaxCrashCount" type="UInt">
<default>3</default>
</entry>
<entry name="XwaylandEavesdrops" type="Enum">
<choices>
<choice name="None"/>
<choice name="Modifiers"/>
<choice name="Combinations"/>
<choice name="All"/>
</choices>
<default>None</default>
</entry>
</group>
</kcfg>

View file

@ -20,6 +20,7 @@
#include <QProcess>
#include "settings.h"
#include "workspace.h"
#include <QOpenGLContext>
#include <kwinglplatform.h>
@ -55,6 +56,7 @@ Options::Options(QObject *parent)
, m_hideUtilityWindowsForInactive(false)
, m_xwaylandCrashPolicy(Options::defaultXwaylandCrashPolicy())
, m_xwaylandMaxCrashCount(Options::defaultXwaylandMaxCrashCount())
, m_xwaylandEavesdrops(Options::defaultXwaylandEavesdrops())
, m_latencyPolicy(Options::defaultLatencyPolicy())
, m_renderTimeEstimator(Options::defaultRenderTimeEstimator())
, m_compositingMode(Options::defaultCompositingMode())
@ -97,6 +99,8 @@ Options::Options(QObject *parent)
connect(m_configWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
if (group.name() == QLatin1String("KDE") && names.contains(QByteArrayLiteral("AnimationDurationFactor"))) {
Q_EMIT animationSpeedChanged();
} else if (group.name() == QLatin1String("Xwayland")) {
workspace()->reconfigure();
}
});
}
@ -146,6 +150,15 @@ void Options::setXwaylandMaxCrashCount(int maxCrashCount)
Q_EMIT xwaylandMaxCrashCountChanged();
}
void Options::setXwaylandEavesdrops(XwaylandEavesdropsMode mode)
{
if (m_xwaylandEavesdrops == mode) {
return;
}
m_xwaylandEavesdrops = mode;
Q_EMIT xwaylandEavesdropsChanged();
}
void Options::setClickRaise(bool clickRaise)
{
if (m_autoRaise) {
@ -785,6 +798,7 @@ void Options::syncFromKcfgc()
setActivationDesktopPolicy(m_settings->activationDesktopPolicy());
setXwaylandCrashPolicy(m_settings->xwaylandCrashPolicy());
setXwaylandMaxCrashCount(m_settings->xwaylandMaxCrashCount());
setXwaylandEavesdrops(XwaylandEavesdropsMode(m_settings->xwaylandEavesdrops()));
setPlacement(m_settings->placement());
setAutoRaise(m_settings->autoRaise());
setAutoRaiseInterval(m_settings->autoRaiseInterval());

View file

@ -33,6 +33,13 @@ enum HiddenPreviews {
HiddenPreviewsAlways
};
enum XwaylandEavesdropsMode {
None,
Modifiers,
Combinations,
All
};
/**
* This enum type specifies whether the Xwayland server must be restarted after a crash.
*/
@ -275,6 +282,10 @@ public:
{
return m_xwaylandMaxCrashCount;
}
XwaylandEavesdropsMode xwaylandEavesdrops() const
{
return m_xwaylandEavesdrops;
}
/**
* Whether clicking on a window raises it in FocusFollowsMouse
@ -711,6 +722,7 @@ public:
void setFocusPolicy(FocusPolicy focusPolicy);
void setXwaylandCrashPolicy(XwaylandCrashPolicy crashPolicy);
void setXwaylandMaxCrashCount(int maxCrashCount);
void setXwaylandEavesdrops(XwaylandEavesdropsMode mode);
void setNextFocusPrefersMouse(bool nextFocusPrefersMouse);
void setClickRaise(bool clickRaise);
void setAutoRaise(bool autoRaise);
@ -888,6 +900,10 @@ public:
{
return 3;
}
static XwaylandEavesdropsMode defaultXwaylandEavesdrops()
{
return None;
}
static LatencyPolicy defaultLatencyPolicy()
{
return LatencyMedium;
@ -917,6 +933,7 @@ Q_SIGNALS:
void focusPolicyIsResonableChanged();
void xwaylandCrashPolicyChanged();
void xwaylandMaxCrashCountChanged();
void xwaylandEavesdropsChanged();
void nextFocusPrefersMouseChanged();
void clickRaiseChanged();
void autoRaiseChanged();
@ -1002,6 +1019,7 @@ private:
bool m_hideUtilityWindowsForInactive;
XwaylandCrashPolicy m_xwaylandCrashPolicy;
int m_xwaylandMaxCrashCount;
XwaylandEavesdropsMode m_xwaylandEavesdrops;
LatencyPolicy m_latencyPolicy;
RenderTimeEstimator m_renderTimeEstimator;

View file

@ -169,6 +169,15 @@ QVector<quint32> KeyboardInterfacePrivate::pressedKeys() const
return keys;
}
void KeyboardInterface::sendKey(quint32 key, KeyboardKeyState state, ClientConnection *client)
{
const QList<KeyboardInterfacePrivate::Resource *> keyboards = d->keyboardsForClient(client);
const quint32 serial = d->seat->display()->nextSerial();
for (KeyboardInterfacePrivate::Resource *keyboardResource : keyboards) {
d->send_key(keyboardResource->handle, serial, d->seat->timestamp(), key, quint32(state));
}
}
void KeyboardInterface::sendKey(quint32 key, KeyboardKeyState state)
{
if (!d->updateKey(key, state)) {
@ -179,11 +188,7 @@ void KeyboardInterface::sendKey(quint32 key, KeyboardKeyState state)
return;
}
const QList<KeyboardInterfacePrivate::Resource *> keyboards = d->keyboardsForClient(d->focusedSurface->client());
const quint32 serial = d->seat->display()->nextSerial();
for (KeyboardInterfacePrivate::Resource *keyboardResource : keyboards) {
d->send_key(keyboardResource->handle, serial, d->seat->timestamp(), key, quint32(state));
}
sendKey(key, state, d->focusedSurface->client());
}
void KeyboardInterface::sendModifiers(quint32 depressed, quint32 latched, quint32 locked, quint32 group)

View file

@ -11,6 +11,7 @@
namespace KWaylandServer
{
class ClientConnection;
class SeatInterface;
class SurfaceInterface;
class KeyboardInterfacePrivate;
@ -54,6 +55,7 @@ public:
void setRepeatInfo(qint32 charactersPerSecond, qint32 delay);
void sendKey(quint32 key, KeyboardKeyState state);
void sendKey(quint32 key, KeyboardKeyState state, ClientConnection *client);
void sendModifiers(quint32 depressed, quint32 latched, quint32 locked, quint32 group);
private:

View file

@ -908,6 +908,7 @@ void SeatInterface::setFocusedKeyboardSurface(SurfaceInterface *surface)
return;
}
Q_EMIT focusedKeyboardSurfaceAboutToChange(surface);
const quint32 serial = d->display->nextSerial();
if (d->globalKeyboard.focus.surface) {

View file

@ -720,6 +720,11 @@ Q_SIGNALS:
* @see focusedTextInput
*/
void focusedTextInputSurfaceChanged();
/**
* Emitted whenever the focused keyboard is about to change.
* @see focusedKeyboardSurface
*/
void focusedKeyboardSurfaceAboutToChange(SurfaceInterface *nextSurface);
private:
std::unique_ptr<SeatInterfacePrivate> d;

View file

@ -15,19 +15,27 @@
#include "cursor.h"
#include "databridge.h"
#include "dnd.h"
#include "window.h"
#include "xwaylandlauncher.h"
#include "xwldrophandler.h"
#include "core/output.h"
#include "input_event_spy.h"
#include "keyboard_input.h"
#include "main_wayland.h"
#include "utils/common.h"
#include "utils/xcbutils.h"
#include "wayland_server.h"
#include "waylandwindow.h"
#include "workspace.h"
#include "x11eventfilter.h"
#include "xkb.h"
#include "xwayland_logging.h"
#include <KSelectionOwner>
#include <wayland/keyboard_interface.h>
#include <wayland/seat_interface.h>
#include <wayland/surface_interface.h>
#include <QAbstractEventDispatcher>
#include <QDataStream>
@ -41,8 +49,10 @@
#include <cerrno>
#include <cstring>
#include <input_event.h>
#include <sys/socket.h>
#include <unistd.h>
#include <xkbcommon/xkbcommon-keysyms.h>
namespace KWin
{
@ -73,6 +83,116 @@ bool XrandrEventFilter::event(xcb_generic_event_t *event)
return false;
}
class XwaylandInputSpy : public QObject, public KWin::InputEventSpy
{
public:
XwaylandInputSpy()
{
connect(waylandServer()->seat(), &KWaylandServer::SeatInterface::focusedKeyboardSurfaceAboutToChange,
this, [this](KWaylandServer::SurfaceInterface *newSurface) {
auto keyboard = waylandServer()->seat()->keyboard();
if (!newSurface) {
return;
}
if (waylandServer()->xWaylandConnection() == newSurface->client()) {
// Since this is a spy but the keyboard interface gets its normal sendKey calls through filters,
// there can be a mismatch in both states.
// This loop makes sure all key press events are reset before we switch back to the
// Xwayland client and the state is correctly restored.
for (auto it = m_states.constBegin(); it != m_states.constEnd(); ++it) {
if (it.value() == KWaylandServer::KeyboardKeyState::Pressed) {
keyboard->sendKey(it.key(), KWaylandServer::KeyboardKeyState::Released, waylandServer()->xWaylandConnection());
}
}
m_states.clear();
}
});
}
void setMode(XwaylandEavesdropsMode mode)
{
static const QSet<quint32> modifierKeys = {
Qt::Key_Control,
Qt::Key_Shift,
Qt::Key_Alt,
Qt::Key_Meta,
};
switch (mode) {
case None:
m_filter = {};
break;
case Modifiers:
m_filter = [](int key, Qt::KeyboardModifiers) {
return modifierKeys.contains(key);
};
break;
case Combinations:
m_filter = [](int key, Qt::KeyboardModifiers m) {
return m != Qt::NoModifier || modifierKeys.contains(key);
};
break;
case All:
m_filter = [](int, Qt::KeyboardModifiers) {
return true;
};
break;
}
}
void keyEvent(KWin::KeyEvent *event) override
{
if (event->isAutoRepeat()) {
return;
}
Window *window = workspace()->activeWindow();
if (!m_filter || !m_filter(event->key(), event->modifiers()) || (window && window->isLockScreen())) {
return;
}
auto keyboard = waylandServer()->seat()->keyboard();
auto surface = keyboard->focusedSurface();
if (!surface) {
return;
}
auto client = surface->client();
if (waylandServer()->xWaylandConnection() != client) {
KWaylandServer::KeyboardKeyState state{event->type() == QEvent::KeyPress};
if (!updateKey(event->nativeScanCode(), state)) {
return;
}
auto xkb = input()->keyboard()->xkb();
keyboard->sendModifiers(xkb->modifierState().depressed,
xkb->modifierState().latched,
xkb->modifierState().locked,
xkb->currentLayout());
waylandServer()->seat()->keyboard()->sendKey(event->nativeScanCode(), state, waylandServer()->xWaylandConnection());
}
}
bool updateKey(quint32 key, KWaylandServer::KeyboardKeyState state)
{
auto it = m_states.find(key);
if (it == m_states.end()) {
m_states.insert(key, state);
return true;
}
if (it.value() == state) {
return false;
}
it.value() = state;
return true;
}
QHash<quint32, KWaylandServer::KeyboardKeyState> m_states;
std::function<bool(int key, Qt::KeyboardModifiers)> m_filter;
};
Xwayland::Xwayland(Application *app)
: m_app(app)
, m_launcher(new XwaylandLauncher(this))
@ -205,6 +325,33 @@ void Xwayland::handleXwaylandReady()
delete m_xrandrEventsFilter;
m_xrandrEventsFilter = new XrandrEventFilter(this);
refreshEavesdropping();
connect(options, &Options::xwaylandEavesdropsChanged, this, &Xwayland::refreshEavesdropping);
}
void Xwayland::refreshEavesdropping()
{
if (!waylandServer()->seat()->keyboard()) {
return;
}
const bool enabled = options->xwaylandEavesdrops() != None;
if (enabled == bool(m_inputSpy)) {
if (m_inputSpy) {
m_inputSpy->setMode(options->xwaylandEavesdrops());
}
return;
}
if (enabled) {
m_inputSpy.reset(new XwaylandInputSpy);
input()->installInputEventSpy(m_inputSpy.get());
m_inputSpy->setMode(options->xwaylandEavesdrops());
} else {
input()->uninstallInputEventSpy(m_inputSpy.get());
m_inputSpy.reset();
}
}
void Xwayland::updatePrimary()

View file

@ -27,6 +27,7 @@ class Application;
namespace Xwl
{
class XrandrEventFilter;
class XwaylandInputSpy;
class XwaylandLauncher;
class DataBridge;
@ -69,6 +70,7 @@ private:
void installSocketNotifier();
void uninstallSocketNotifier();
void updatePrimary();
void refreshEavesdropping();
bool createX11Connection();
void destroyX11Connection();
@ -83,6 +85,7 @@ private:
XrandrEventFilter *m_xrandrEventsFilter = nullptr;
XwaylandLauncher *m_launcher;
std::unique_ptr<XwaylandInputSpy> m_inputSpy;
Q_DISABLE_COPY(Xwayland)
};

View file

@ -90,6 +90,7 @@ private Q_SLOTS:
private:
void maybeDestroyReadyNotifier();
bool startInternal();
void stopInternal();
void restartInternal();