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:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user