Type into any window: fake input when no text field is focused

- KWinFakeInput (org_kde_kwin_fake_input v6, offered by KWin to the
  input-method process): keysyms with real modifier keys held
- The KWin backend uses the input method in text fields and fake input
  everywhere else, so shortcuts, terminals and the desktop get keys
- Text rules (auto-capitalization, double-space) only apply in fields
- Always Show setting keeps the keyboard up without a text field

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 21:59:32 +02:00
co-authored by Claude Opus 5.5
parent ba2f17d665
commit f816135162
20 changed files with 303 additions and 23 deletions
+2
View File
@@ -78,12 +78,14 @@ qt_add_executable(sloposk
backend/InputMethodProtocol.h
backend/KWinInputMethod.h backend/KWinInputMethod.cpp
backend/KWinInputContext.h backend/KWinInputContext.cpp
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
backend/WaylandKeymap.h backend/WaylandKeymap.cpp
window/FloatingWindow.h window/FloatingWindow.cpp
)
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
${WAYLAND_PROTOCOLS_DIR}/unstable/input-method/input-method-unstable-v1.xml
${PROJECT_SOURCE_DIR}/protocols/fake-input.xml
)
# Generated code lacks the core-protocol include (see backend/InputMethodProtocol.h).
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/qwayland-input-method-unstable-v1.cpp
+60
View File
@@ -0,0 +1,60 @@
#include "KWinFakeInput.h"
#include <xkbcommon/xkbcommon.h>
namespace {
constexpr uint32_t Released = 0;
constexpr uint32_t Pressed = 1;
// Modifiers are held with real keys (evdev codes) so they combine with any keysym.
struct ModifierKey {
Qt::KeyboardModifier flag;
uint32_t evdevCode;
};
constexpr ModifierKey ModifierKeys[] = {
{Qt::ControlModifier, 29}, // KEY_LEFTCTRL
{Qt::ShiftModifier, 42}, // KEY_LEFTSHIFT
{Qt::AltModifier, 56}, // KEY_LEFTALT
{Qt::MetaModifier, 125}, // KEY_LEFTMETA
};
} // namespace
KWinFakeInput::KWinFakeInput()
: QWaylandClientExtensionTemplate(6) // 6: keyboard_keysym
{
connect(this, &QWaylandClientExtension::activeChanged, this, [this] {
if (isActive())
authenticate(QStringLiteral("sloposk"), QStringLiteral("On-screen keyboard: type into any window"));
});
initialize();
}
KWinFakeInput::~KWinFakeInput()
{
if (isActive())
destroy();
}
void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
{
if (!isActive())
return;
for (const ModifierKey &key : ModifierKeys) {
if (modifiers & key.flag)
keyboard_key(key.evdevCode, Pressed);
}
keyboard_keysym(keysym, Pressed);
keyboard_keysym(keysym, Released);
for (const ModifierKey &key : ModifierKeys) {
if (modifiers & key.flag)
keyboard_key(key.evdevCode, Released);
}
}
void KWinFakeInput::typeText(const QString &text)
{
for (const char32_t ucs : text.toUcs4())
sendKeysym(xkb_utf32_to_keysym(ucs));
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QWaylandClientExtensionTemplate>
#include <qnamespace.h>
#include "qwayland-fake-input.h"
// Key events for whatever window has focus, as from a hardware keyboard:
// KWin's fake-input protocol, which it offers to the input-method process.
// Used when no text field is focused, so shortcuts, terminals and games work.
class KWinFakeInput : public QWaylandClientExtensionTemplate<KWinFakeInput>, public QtWayland::org_kde_kwin_fake_input
{
Q_OBJECT
public:
KWinFakeInput();
~KWinFakeInput() override;
// Presses and releases `keysym` with `modifiers` held. KWin finds the key
// (and any shift level) in the current keymap.
void sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
// Types each character as its keysym.
void typeText(const QString &text);
};
+1
View File
@@ -40,6 +40,7 @@ KWinInputContext::KWinInputContext(::zwp_input_method_context_v1 *object, QObjec
: QObject(parent)
, QtWayland::zwp_input_method_context_v1(object)
{
m_context.field = true;
announceModifiers();
}
+13 -1
View File
@@ -1,6 +1,8 @@
#include "KWinInputMethod.h"
#include "KWinInputContext.h"
#include <xkbcommon/xkbcommon-keysyms.h>
KWinInputMethodGlobal::KWinInputMethodGlobal()
: QWaylandClientExtensionTemplate(1)
{
@@ -46,12 +48,20 @@ void KWinInputMethod::commitText(const QString &text)
{
if (m_context)
m_context->commit(text);
else
m_fakeInput.typeText(text);
}
void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text)
{
if (m_context)
if (m_context) {
m_context->replaceBeforeCursor(before, text);
return;
}
// Key events are one ordered stream, so plain Backspaces are safe here.
for (qsizetype i = 0; i < before.toUcs4().size(); ++i)
m_fakeInput.sendKeysym(XKB_KEY_BackSpace);
m_fakeInput.typeText(text);
}
void KWinInputMethod::setPreedit(const QString &text)
@@ -64,6 +74,8 @@ void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers
{
if (m_context)
m_context->sendKeysym(keysym, modifiers);
else
m_fakeInput.sendKeysym(keysym, modifiers);
}
void KWinInputMethod::activate(::zwp_input_method_context_v1 *context)
+5
View File
@@ -1,5 +1,6 @@
#pragma once
#include "KWinFakeInput.h"
#include "TextBackend.h"
#include "InputMethodProtocol.h"
@@ -28,6 +29,9 @@ protected:
void zwp_input_method_v1_deactivate(::zwp_input_method_context_v1 *context) override;
};
// Types into the focused text field through the input method; with none
// focused, sends key events to the focused window through fake input, so the
// keyboard works everywhere (shortcuts, terminals, the desktop).
class KWinInputMethod : public TextBackend
{
Q_OBJECT
@@ -55,5 +59,6 @@ private:
void deactivate();
KWinInputMethodGlobal m_global;
KWinFakeInput m_fakeInput;
std::unique_ptr<KWinInputContext> m_context;
};
+2 -1
View File
@@ -23,6 +23,7 @@ struct TextContext {
Multiline = 0x200,
};
bool field = false; // a text field is focused; without one, keys go to the window as from a keyboard
bool hasSurroundingText = false;
QString textBeforeCursor;
Purpose purpose = Purpose::Normal;
@@ -33,7 +34,7 @@ struct TextContext {
{
const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha
|| purpose == Purpose::Name;
return prosePurpose && !(hints & (HiddenText | SensitiveData));
return field && prosePurpose && !(hints & (HiddenText | SensitiveData));
}
bool allowsAutoCapitalization() const { return isProse() && !(hints & Lowercase); }
+2 -3
View File
@@ -141,12 +141,11 @@ void KeyboardController::moveCursor(QPoint steps)
repeat(qMax(0, -steps.y()), XKB_KEY_Up);
}
void KeyboardController::resetContext()
void KeyboardController::resetContext(const TextContext &context)
{
m_deadKeys.cancel();
m_context = {};
m_textBefore.clear();
autoCapitalize();
setContext(context);
}
void KeyboardController::setContext(const TextContext &context)
+3 -2
View File
@@ -53,8 +53,9 @@ public:
// Space-bar trackpad: move the text cursor by whole steps.
void moveCursor(QPoint steps);
// A field was focused or unfocused: forget what we knew about the text.
void resetContext();
// A field was focused (context.field) or unfocused: forget what we knew
// about the text and start over from what it reported.
void resetContext(const TextContext &context);
// The focused field reported new text/content type.
void setContext(const TextContext &context);
void setOptions(const TypingOptions &options);
+7 -3
View File
@@ -85,9 +85,12 @@ void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settin
[&] { settings.save(placement.screenName(), placement.geometry()); });
}
void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend,
void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, const Settings &settings,
KeyboardController &controller, FloatingWindow &placement)
{
visibility.setAlwaysVisible(settings.alwaysVisible());
QObject::connect(&settings, &Settings::alwaysVisibleChanged, &visibility,
[&] { visibility.setAlwaysVisible(settings.alwaysVisible()); });
// Without the input-method global (run outside KWin's control, e.g. for UI
// work) there is no focus signal: stay visible so it can be tried out.
const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); };
@@ -102,7 +105,8 @@ void connectVisibility(VisibilityController &visibility, KWinInputMethod &backen
void connectTextContext(KWinInputMethod &backend, KeyboardController &controller)
{
QObject::connect(&backend, &TextBackend::activeChanged, &controller, &KeyboardController::resetContext);
QObject::connect(&backend, &TextBackend::activeChanged, &controller,
[&] { controller.resetContext(backend.context()); });
QObject::connect(&backend, &TextBackend::contextChanged, &controller,
[&] { controller.setContext(backend.context()); });
}
@@ -158,7 +162,7 @@ int main(int argc, char *argv[])
connectKeySettings(settings, model, placement, *window, rebuildLayout);
VisibilityController visibility;
connectVisibility(visibility, backend, controller, placement);
connectVisibility(visibility, backend, settings, controller, placement);
DBusControl dbus(visibility);
if (!dbus.registerOnSessionBus())
+11
View File
@@ -9,6 +9,7 @@ constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
constexpr auto VisibleSectionsKey = "layout/visibleSections";
constexpr auto FullKeyboardKey = "layout/fullKeyboard";
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
}
Settings::Settings(QObject *parent)
@@ -22,6 +23,16 @@ Settings::Settings(QObject *parent)
if (m_visibleSections.isEmpty())
m_visibleSections = {MainSectionId};
m_fullKeyboard = store.value(FullKeyboardKey, false).toBool();
m_alwaysVisible = store.value(AlwaysVisibleKey, false).toBool();
}
void Settings::setAlwaysVisible(bool always)
{
if (always == m_alwaysVisible)
return;
m_alwaysVisible = always;
QSettings().setValue(AlwaysVisibleKey, always);
Q_EMIT alwaysVisibleChanged();
}
void Settings::setFullKeyboard(bool full)
+6
View File
@@ -19,6 +19,8 @@ class Settings : public QObject
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
// iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one.
Q_PROPERTY(bool fullKeyboard READ fullKeyboard WRITE setFullKeyboard NOTIFY fullKeyboardChanged)
// Shown even without a focused text field, to type into any window.
Q_PROPERTY(bool alwaysVisible READ alwaysVisible WRITE setAlwaysVisible NOTIFY alwaysVisibleChanged)
public:
explicit Settings(QObject *parent = nullptr);
@@ -36,11 +38,14 @@ public:
bool fullKeyboard() const { return m_fullKeyboard; }
void setFullKeyboard(bool full);
bool alwaysVisible() const { return m_alwaysVisible; }
void setAlwaysVisible(bool always);
Q_SIGNALS:
void typingOptionsChanged();
void visibleSectionsChanged();
void fullKeyboardChanged();
void alwaysVisibleChanged();
private:
void setTypingOptions(const TypingOptions &options);
@@ -48,4 +53,5 @@ private:
TypingOptions m_typing;
QStringList m_visibleSections;
bool m_fullKeyboard = false;
bool m_alwaysVisible = false;
};
+8 -1
View File
@@ -15,6 +15,13 @@ void VisibilityController::setStandalone(bool standalone)
update();
}
void VisibilityController::setAlwaysVisible(bool always)
{
m_alwaysVisible = always;
m_override.reset();
update();
}
void VisibilityController::show()
{
m_override = true;
@@ -35,7 +42,7 @@ void VisibilityController::toggle()
void VisibilityController::update()
{
const bool visible = m_override.value_or(m_standalone || m_fieldActive);
const bool visible = m_override.value_or(m_alwaysVisible || m_standalone || m_fieldActive);
if (visible == m_visible)
return;
m_visible = visible;
+4 -1
View File
@@ -5,7 +5,8 @@
// Decides whether the keyboard is shown:
// - while a text field is focused
// - always, when running standalone (no input-method connection)
// - always, if the user wants it (to type into any window) or when running
// standalone (no input-method connection)
// - show()/hide()/toggle() override that until the focus changes
class VisibilityController : public QObject
{
@@ -18,6 +19,7 @@ public:
void setFieldActive(bool active);
void setStandalone(bool standalone);
void setAlwaysVisible(bool always);
void show();
void hide();
@@ -31,6 +33,7 @@ private:
bool m_fieldActive = false;
bool m_standalone = false;
bool m_alwaysVisible = false;
std::optional<bool> m_override;
bool m_visible = false;
};