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
+5 -2
View File
@@ -4,6 +4,7 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U
- Flick a key down for its secondary symbol; long-press for accented alternates - Flick a key down for its secondary symbol; long-press for accented alternates
- iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held - iOS shift: tap = one letter, double-tap = caps lock, hold = shift while held
- Works everywhere, not just in text fields: with none focused, keys reach the focused window like a hardware keyboard (shortcuts, terminals, the desktop); "Always Show" in ⚙ keeps it up
- Letters follow the system keyboard layout; the globe key switches it - Letters follow the system keyboard layout; the globe key switches it
- Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete - Auto-capitalization, double-space → ". " (both optional, ⚙ on the bottom bar), backspace repeat → word delete
- Long-press space = trackpad; slide from `.?123` onto a symbol and release - Long-press space = trackpad; slide from `.?123` onto a symbol and release
@@ -60,7 +61,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
## Settings ## Settings
`~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`. `~/.config/sloposk/sloposk.conf`, also editable from the ⚙ panel: `typing/autoCapitalize`, `typing/doubleSpacePeriod`, `layout/visibleSections` (`main` plus section ids), `layout/fullKeyboard`, `window/alwaysVisible`.
## Architecture ## Architecture
@@ -68,7 +69,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
qml/ view only qml/ view only
input/ touch → PointerTracker gestures → KeyboardController rules input/ touch → PointerTracker gestures → KeyboardController rules
engine/ key output → text ops (DirectEngine; prediction/swipe later) engine/ key output → text ops (DirectEngine; prediction/swipe later)
backend/ text ops → compositor (KWin input-method-v1); the compositor's keymap backend/ text ops → compositor (KWin input-method-v1, or fake input with no text field); the compositor's keymap
layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing) layout/ template JSON + keymap letters → Layout → LayoutModel (geometry, hit testing)
window/ floating layer-shell placement, visibility, saved geometry window/ floating layer-shell placement, visibility, saved geometry
app/ D-Bus control and system layouts, dev hooks app/ D-Bus control and system layouts, dev hooks
@@ -76,3 +77,5 @@ settings/ persisted user preferences
``` ```
`sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests. `sloposk_core` (everything compositor-independent, plus the `Sloposk` QML module) is a static library shared by the app and the tests.
`protocols/fake-input.xml` is KDE's `org_kde_kwin_fake_input` (LGPL-2.1-or-later), vendored from plasma-wayland-protocols.
+2
View File
@@ -6,3 +6,5 @@ Type=Application
Icon=input-keyboard-virtual Icon=input-keyboard-virtual
NoDisplay=true NoDisplay=true
X-KDE-Wayland-VirtualKeyboard=true X-KDE-Wayland-VirtualKeyboard=true
# Key events for windows without a text field (fake input is restricted).
X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input
+103
View File
@@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="fake_input">
<copyright><![CDATA[
SPDX-FileCopyrightText: 2015 Martin Gräßlin
SPDX-License-Identifier: LGPL-2.1-or-later
]]></copyright>
<interface name="org_kde_kwin_fake_input" version="6">
<description summary="Fake input manager">
This interface allows other processes to provide fake input events.
Purpose is on the one hand side to provide testing facilities like XTest on X11.
But also to support use case like kdeconnect's mouse pad interface.
A compositor should not trust the input received from this interface.
Clients should not expect that the compositor honors the requests from this
interface.
Warning! The protocol described in this file is a desktop environment
implementation detail. Regular clients must not use this protocol.
Backward incompatible changes may be added without bumping the major
version of the extension.
</description>
<request name="authenticate">
<description summary="Information why the client wants to use the interface">
A client should use this request to tell the compositor why it wants to
use this interface. The compositor might use the information to decide
whether it wants to grant the request. The data might also be passed to
the user to decide whether the application should get granted access to
this very privileged interface.
</description>
<arg name="application" type="string" summary="user visible name of the application"/>
<arg name="reason" type="string" summary="reason why the application wants to use this interface"/>
</request>
<request name="pointer_motion">
<arg name="delta_x" type="fixed"/>
<arg name="delta_y" type="fixed"/>
</request>
<request name="button">
<arg name="button" type="uint"/>
<arg name="state" type="uint"/>
</request>
<request name="axis">
<arg name="axis" type="uint"/>
<arg name="value" type="fixed"/>
</request>
<request name="touch_down" since="2">
<description summary="touch down event">
A client should use this request to send touch down event at specific
coordinates.
</description>
<arg name="id" type="uint" summary="unique id for touch down event"/>
<arg name="x" type="fixed" summary="x coordinate for touch down event"/>
<arg name="y" type="fixed" summary="y coordinate for touch down event"/>
</request>
<request name="touch_motion" since="2">
<description summary="touch motion event">
A client should use this request to send touch motion to specific position.
</description>
<arg name="id" type="uint" summary="unique id for touch motion event"/>
<arg name="x" type="fixed" summary="x coordinate for touch motion event"/>
<arg name="y" type="fixed" summary="y coordinate for touch motion event"/>
</request>
<request name="touch_up" since="2">
<description summary="touch up event">
A client should use this request to send touch up event.
</description>
<arg name="id" type="uint" summary="unique id for touch up event"/>
</request>
<request name="touch_cancel" since="2">
<description summary="touch cancel event">
A client should use this request to cancel the current
touch event.
</description>
</request>
<request name="touch_frame" since="2">
<description summary="touch frame event">
A client should use this request to send touch frame event.
</description>
</request>
<request name="pointer_motion_absolute" since="3">
<arg name="x" type="fixed"/>
<arg name="y" type="fixed"/>
</request>
<request name="keyboard_key" since="4">
<arg name="button" type="uint"/>
<arg name="state" type="uint"/>
</request>
<!-- Version 5 additions -->
<request name="destroy" type="destructor" since="5">
<description summary="Destroy the fake input device"/>
</request>
<!-- Version 6 additions -->
<request name="keyboard_keysym" since="6">
<arg name="keysym" type="uint"/>
<arg name="state" type="uint"/>
</request>
</interface>
</protocol>
+8
View File
@@ -32,6 +32,14 @@ Rectangle {
onToggled: checked => panel.settings.autoCapitalize = checked onToggled: checked => panel.settings.autoCapitalize = checked
} }
ToggleRow {
width: parent.width
text: qsTr("Always Show")
detail: qsTr("Type into any window, not just text fields")
checked: panel.settings.alwaysVisible
onToggled: checked => panel.settings.alwaysVisible = checked
}
ToggleRow { ToggleRow {
width: parent.width width: parent.width
text: qsTr("“.” Shortcut") text: qsTr("“.” Shortcut")
+2
View File
@@ -78,12 +78,14 @@ qt_add_executable(sloposk
backend/InputMethodProtocol.h backend/InputMethodProtocol.h
backend/KWinInputMethod.h backend/KWinInputMethod.cpp backend/KWinInputMethod.h backend/KWinInputMethod.cpp
backend/KWinInputContext.h backend/KWinInputContext.cpp backend/KWinInputContext.h backend/KWinInputContext.cpp
backend/KWinFakeInput.h backend/KWinFakeInput.cpp
backend/WaylandKeymap.h backend/WaylandKeymap.cpp backend/WaylandKeymap.h backend/WaylandKeymap.cpp
window/FloatingWindow.h window/FloatingWindow.cpp window/FloatingWindow.h window/FloatingWindow.cpp
) )
qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES qt6_generate_wayland_protocol_client_sources(sloposk PRIVATE_CODE FILES
${WAYLAND_PROTOCOLS_DIR}/unstable/input-method/input-method-unstable-v1.xml ${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). # 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 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) : QObject(parent)
, QtWayland::zwp_input_method_context_v1(object) , QtWayland::zwp_input_method_context_v1(object)
{ {
m_context.field = true;
announceModifiers(); announceModifiers();
} }
+13 -1
View File
@@ -1,6 +1,8 @@
#include "KWinInputMethod.h" #include "KWinInputMethod.h"
#include "KWinInputContext.h" #include "KWinInputContext.h"
#include <xkbcommon/xkbcommon-keysyms.h>
KWinInputMethodGlobal::KWinInputMethodGlobal() KWinInputMethodGlobal::KWinInputMethodGlobal()
: QWaylandClientExtensionTemplate(1) : QWaylandClientExtensionTemplate(1)
{ {
@@ -46,12 +48,20 @@ void KWinInputMethod::commitText(const QString &text)
{ {
if (m_context) if (m_context)
m_context->commit(text); m_context->commit(text);
else
m_fakeInput.typeText(text);
} }
void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text) void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &text)
{ {
if (m_context) if (m_context) {
m_context->replaceBeforeCursor(before, text); 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) void KWinInputMethod::setPreedit(const QString &text)
@@ -64,6 +74,8 @@ void KWinInputMethod::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers
{ {
if (m_context) if (m_context)
m_context->sendKeysym(keysym, modifiers); m_context->sendKeysym(keysym, modifiers);
else
m_fakeInput.sendKeysym(keysym, modifiers);
} }
void KWinInputMethod::activate(::zwp_input_method_context_v1 *context) void KWinInputMethod::activate(::zwp_input_method_context_v1 *context)
+5
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "KWinFakeInput.h"
#include "TextBackend.h" #include "TextBackend.h"
#include "InputMethodProtocol.h" #include "InputMethodProtocol.h"
@@ -28,6 +29,9 @@ protected:
void zwp_input_method_v1_deactivate(::zwp_input_method_context_v1 *context) override; 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 class KWinInputMethod : public TextBackend
{ {
Q_OBJECT Q_OBJECT
@@ -55,5 +59,6 @@ private:
void deactivate(); void deactivate();
KWinInputMethodGlobal m_global; KWinInputMethodGlobal m_global;
KWinFakeInput m_fakeInput;
std::unique_ptr<KWinInputContext> m_context; std::unique_ptr<KWinInputContext> m_context;
}; };
+2 -1
View File
@@ -23,6 +23,7 @@ struct TextContext {
Multiline = 0x200, Multiline = 0x200,
}; };
bool field = false; // a text field is focused; without one, keys go to the window as from a keyboard
bool hasSurroundingText = false; bool hasSurroundingText = false;
QString textBeforeCursor; QString textBeforeCursor;
Purpose purpose = Purpose::Normal; Purpose purpose = Purpose::Normal;
@@ -33,7 +34,7 @@ struct TextContext {
{ {
const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha
|| purpose == Purpose::Name; || purpose == Purpose::Name;
return prosePurpose && !(hints & (HiddenText | SensitiveData)); return field && prosePurpose && !(hints & (HiddenText | SensitiveData));
} }
bool allowsAutoCapitalization() const { return isProse() && !(hints & Lowercase); } 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); repeat(qMax(0, -steps.y()), XKB_KEY_Up);
} }
void KeyboardController::resetContext() void KeyboardController::resetContext(const TextContext &context)
{ {
m_deadKeys.cancel(); m_deadKeys.cancel();
m_context = {};
m_textBefore.clear(); m_textBefore.clear();
autoCapitalize(); setContext(context);
} }
void KeyboardController::setContext(const TextContext &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. // Space-bar trackpad: move the text cursor by whole steps.
void moveCursor(QPoint steps); void moveCursor(QPoint steps);
// A field was focused or unfocused: forget what we knew about the text. // A field was focused (context.field) or unfocused: forget what we knew
void resetContext(); // about the text and start over from what it reported.
void resetContext(const TextContext &context);
// The focused field reported new text/content type. // The focused field reported new text/content type.
void setContext(const TextContext &context); void setContext(const TextContext &context);
void setOptions(const TypingOptions &options); 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()); }); [&] { 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) 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 // 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. // work) there is no focus signal: stay visible so it can be tried out.
const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); }; const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); };
@@ -102,7 +105,8 @@ void connectVisibility(VisibilityController &visibility, KWinInputMethod &backen
void connectTextContext(KWinInputMethod &backend, KeyboardController &controller) 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, QObject::connect(&backend, &TextBackend::contextChanged, &controller,
[&] { controller.setContext(backend.context()); }); [&] { controller.setContext(backend.context()); });
} }
@@ -158,7 +162,7 @@ int main(int argc, char *argv[])
connectKeySettings(settings, model, placement, *window, rebuildLayout); connectKeySettings(settings, model, placement, *window, rebuildLayout);
VisibilityController visibility; VisibilityController visibility;
connectVisibility(visibility, backend, controller, placement); connectVisibility(visibility, backend, settings, controller, placement);
DBusControl dbus(visibility); DBusControl dbus(visibility);
if (!dbus.registerOnSessionBus()) if (!dbus.registerOnSessionBus())
+11
View File
@@ -9,6 +9,7 @@ constexpr auto AutoCapitalizeKey = "typing/autoCapitalize";
constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod"; constexpr auto DoubleSpacePeriodKey = "typing/doubleSpacePeriod";
constexpr auto VisibleSectionsKey = "layout/visibleSections"; constexpr auto VisibleSectionsKey = "layout/visibleSections";
constexpr auto FullKeyboardKey = "layout/fullKeyboard"; constexpr auto FullKeyboardKey = "layout/fullKeyboard";
constexpr auto AlwaysVisibleKey = "window/alwaysVisible";
} }
Settings::Settings(QObject *parent) Settings::Settings(QObject *parent)
@@ -22,6 +23,16 @@ Settings::Settings(QObject *parent)
if (m_visibleSections.isEmpty()) if (m_visibleSections.isEmpty())
m_visibleSections = {MainSectionId}; m_visibleSections = {MainSectionId};
m_fullKeyboard = store.value(FullKeyboardKey, false).toBool(); 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) void Settings::setFullKeyboard(bool full)
+6
View File
@@ -19,6 +19,8 @@ class Settings : public QObject
Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged) Q_PROPERTY(QStringList visibleSections READ visibleSections NOTIFY visibleSectionsChanged)
// iPad Pro-style full keyboard (number row, tab, ctrl/alt/super) instead of the compact one. // 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) 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: public:
explicit Settings(QObject *parent = nullptr); explicit Settings(QObject *parent = nullptr);
@@ -36,11 +38,14 @@ public:
bool fullKeyboard() const { return m_fullKeyboard; } bool fullKeyboard() const { return m_fullKeyboard; }
void setFullKeyboard(bool full); void setFullKeyboard(bool full);
bool alwaysVisible() const { return m_alwaysVisible; }
void setAlwaysVisible(bool always);
Q_SIGNALS: Q_SIGNALS:
void typingOptionsChanged(); void typingOptionsChanged();
void visibleSectionsChanged(); void visibleSectionsChanged();
void fullKeyboardChanged(); void fullKeyboardChanged();
void alwaysVisibleChanged();
private: private:
void setTypingOptions(const TypingOptions &options); void setTypingOptions(const TypingOptions &options);
@@ -48,4 +53,5 @@ private:
TypingOptions m_typing; TypingOptions m_typing;
QStringList m_visibleSections; QStringList m_visibleSections;
bool m_fullKeyboard = false; bool m_fullKeyboard = false;
bool m_alwaysVisible = false;
}; };
+8 -1
View File
@@ -15,6 +15,13 @@ void VisibilityController::setStandalone(bool standalone)
update(); update();
} }
void VisibilityController::setAlwaysVisible(bool always)
{
m_alwaysVisible = always;
m_override.reset();
update();
}
void VisibilityController::show() void VisibilityController::show()
{ {
m_override = true; m_override = true;
@@ -35,7 +42,7 @@ void VisibilityController::toggle()
void VisibilityController::update() 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) if (visible == m_visible)
return; return;
m_visible = visible; m_visible = visible;
+4 -1
View File
@@ -5,7 +5,8 @@
// Decides whether the keyboard is shown: // Decides whether the keyboard is shown:
// - while a text field is focused // - 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 // - show()/hide()/toggle() override that until the focus changes
class VisibilityController : public QObject class VisibilityController : public QObject
{ {
@@ -18,6 +19,7 @@ public:
void setFieldActive(bool active); void setFieldActive(bool active);
void setStandalone(bool standalone); void setStandalone(bool standalone);
void setAlwaysVisible(bool always);
void show(); void show();
void hide(); void hide();
@@ -31,6 +33,7 @@ private:
bool m_fieldActive = false; bool m_fieldActive = false;
bool m_standalone = false; bool m_standalone = false;
bool m_alwaysVisible = false;
std::optional<bool> m_override; std::optional<bool> m_override;
bool m_visible = false; bool m_visible = false;
}; };
+25 -9
View File
@@ -10,6 +10,10 @@ using namespace Qt::StringLiterals;
namespace { namespace {
const TextContext Field{.field = true};
// A field with some text already: no auto-shift until a test asks for it.
const TextContext MidSentence{.field = true, .hasSurroundingText = true, .textBeforeCursor = u"so"_s};
KeyDef makeKey(KeyType type, const QString &label = {}) KeyDef makeKey(KeyType type, const QString &label = {})
{ {
KeyDef k; KeyDef k;
@@ -44,6 +48,7 @@ private Q_SLOTS:
model.setLayout(TestLayouts::build("us")); model.setLayout(TestLayouts::build("us"));
model.setVisibleSections({MainSectionId}); model.setVisibleSections({MainSectionId});
controller = std::make_unique<KeyboardController>(engine, model); controller = std::make_unique<KeyboardController>(engine, model);
controller->resetContext(MidSentence); // most tests type into a text field
} }
void typesLowercase() void typesLowercase()
@@ -103,7 +108,7 @@ private Q_SLOTS:
void capitalizesAtSentenceStart() void capitalizesAtSentenceStart()
{ {
controller->resetContext(); controller->resetContext(Field);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
for (const char *c : {"h", "i", ".", " "}) for (const char *c : {"h", "i", ".", " "})
tap(makeKey(KeyType::Character, QString::fromLatin1(c))); tap(makeKey(KeyType::Character, QString::fromLatin1(c)));
@@ -114,7 +119,7 @@ private Q_SLOTS:
void noCapitalizationInPasswordFields() void noCapitalizationInPasswordFields()
{ {
TextContext context; TextContext context = Field;
context.purpose = TextContext::Purpose::Password; context.purpose = TextContext::Purpose::Password;
controller->setContext(context); controller->setContext(context);
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
@@ -122,7 +127,7 @@ private Q_SLOTS:
void fieldTextDrivesCapitalization() void fieldTextDrivesCapitalization()
{ {
TextContext context; TextContext context = Field;
context.hasSurroundingText = true; context.hasSurroundingText = true;
context.textBeforeCursor = u"Done! "_s; context.textBeforeCursor = u"Done! "_s;
controller->setContext(context); controller->setContext(context);
@@ -152,12 +157,12 @@ private Q_SLOTS:
tap(makeKey(KeyType::Character, u"a"_s)); tap(makeKey(KeyType::Character, u"a"_s));
tap(space); tap(space);
tap(space); tap(space);
QCOMPARE(engine.output, u"A "); QCOMPARE(engine.output, u"a ");
} }
void autoCapitalizationCanBeTurnedOff() void autoCapitalizationCanBeTurnedOff()
{ {
controller->resetContext(); controller->resetContext(Field);
controller->setOptions({.autoCapitalize = false}); controller->setOptions({.autoCapitalize = false});
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
tap(makeKey(KeyType::Character, u"a"_s)); tap(makeKey(KeyType::Character, u"a"_s));
@@ -166,7 +171,7 @@ private Q_SLOTS:
void doubleTapShiftLocksAtSentenceStart() void doubleTapShiftLocksAtSentenceStart()
{ {
controller->resetContext(); // a field was focused controller->resetContext(Field); // a field was focused
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift
const KeyDef shift = makeKey(KeyType::Shift); const KeyDef shift = makeKey(KeyType::Shift);
controller->keyDown(shift, 5000); controller->keyDown(shift, 5000);
@@ -281,7 +286,7 @@ private Q_SLOTS:
void autoShiftAndCapsLockDontApplyToFunctionKeys() void autoShiftAndCapsLockDontApplyToFunctionKeys()
{ {
controller->resetContext(); // sentence start: auto-shift on controller->resetContext(Field); // sentence start: auto-shift on
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
tap(keysymKey(XKB_KEY_Left)); tap(keysymKey(XKB_KEY_Left));
QCOMPARE(engine.lastModifiers, Qt::NoModifier); QCOMPARE(engine.lastModifiers, Qt::NoModifier);
@@ -303,10 +308,10 @@ private Q_SLOTS:
lj.shifted = u"2"_s; lj.shifted = u"2"_s;
KeyDef one = makeKey(KeyType::Character, u"+"_s); KeyDef one = makeKey(KeyType::Character, u"+"_s);
one.shifted = u"1"_s; one.shifted = u"1"_s;
controller->resetContext(); // sentence start controller->resetContext(Field); // sentence start
tap(one); tap(one);
QCOMPARE(engine.output, u"+"); // not the shifted level QCOMPARE(engine.output, u"+"); // not the shifted level
controller->resetContext(); controller->resetContext(Field);
tap(lj); tap(lj);
QCOMPARE(engine.output, u"+Ľ"); QCOMPARE(engine.output, u"+Ľ");
tap(makeKey(KeyType::Shift)); // the user's shift does pick it tap(makeKey(KeyType::Shift)); // the user's shift does pick it
@@ -325,6 +330,17 @@ private Q_SLOTS:
QVERIFY(engine.output.endsWith(u"é"_s)); QVERIFY(engine.output.endsWith(u"é"_s));
} }
void noTextRulesWithoutAField()
{
controller->resetContext({}); // keys go to a window, not a text field
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
const KeyDef space = makeKey(KeyType::Space);
tap(makeKey(KeyType::Character, u"a"_s));
tap(space);
tap(space);
QCOMPARE(engine.output, u"a ");
}
void dismissEmitsSignal() void dismissEmitsSignal()
{ {
int dismissed = 0; int dismissed = 0;
+12
View File
@@ -27,6 +27,18 @@ private Q_SLOTS:
QVERIFY(v.isVisible()); QVERIFY(v.isVisible());
} }
void alwaysVisibleShowsWithoutField()
{
VisibilityController v;
v.setAlwaysVisible(true);
QVERIFY(v.isVisible());
v.hide(); // dismiss key still works...
QVERIFY(!v.isVisible());
v.setFieldActive(true); // ...until the focus changes
v.setFieldActive(false);
QVERIFY(v.isVisible());
}
void toggleShowsWithoutField() void toggleShowsWithoutField()
{ {
VisibilityController v; VisibilityController v;