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:
@@ -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
|
||||
- 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
|
||||
- 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
|
||||
@@ -60,7 +61,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
||||
|
||||
## 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
|
||||
|
||||
@@ -68,7 +69,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
||||
qml/ view only
|
||||
input/ touch → PointerTracker gestures → KeyboardController rules
|
||||
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)
|
||||
window/ floating layer-shell placement, visibility, saved geometry
|
||||
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.
|
||||
|
||||
`protocols/fake-input.xml` is KDE's `org_kde_kwin_fake_input` (LGPL-2.1-or-later), vendored from plasma-wayland-protocols.
|
||||
|
||||
@@ -6,3 +6,5 @@ Type=Application
|
||||
Icon=input-keyboard-virtual
|
||||
NoDisplay=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
|
||||
|
||||
@@ -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>
|
||||
@@ -32,6 +32,14 @@ Rectangle {
|
||||
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 {
|
||||
width: parent.width
|
||||
text: qsTr("“.” Shortcut")
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,10 @@ using namespace Qt::StringLiterals;
|
||||
|
||||
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 k;
|
||||
@@ -44,6 +48,7 @@ private Q_SLOTS:
|
||||
model.setLayout(TestLayouts::build("us"));
|
||||
model.setVisibleSections({MainSectionId});
|
||||
controller = std::make_unique<KeyboardController>(engine, model);
|
||||
controller->resetContext(MidSentence); // most tests type into a text field
|
||||
}
|
||||
|
||||
void typesLowercase()
|
||||
@@ -103,7 +108,7 @@ private Q_SLOTS:
|
||||
|
||||
void capitalizesAtSentenceStart()
|
||||
{
|
||||
controller->resetContext();
|
||||
controller->resetContext(Field);
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
for (const char *c : {"h", "i", ".", " "})
|
||||
tap(makeKey(KeyType::Character, QString::fromLatin1(c)));
|
||||
@@ -114,7 +119,7 @@ private Q_SLOTS:
|
||||
|
||||
void noCapitalizationInPasswordFields()
|
||||
{
|
||||
TextContext context;
|
||||
TextContext context = Field;
|
||||
context.purpose = TextContext::Purpose::Password;
|
||||
controller->setContext(context);
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
|
||||
@@ -122,7 +127,7 @@ private Q_SLOTS:
|
||||
|
||||
void fieldTextDrivesCapitalization()
|
||||
{
|
||||
TextContext context;
|
||||
TextContext context = Field;
|
||||
context.hasSurroundingText = true;
|
||||
context.textBeforeCursor = u"Done! "_s;
|
||||
controller->setContext(context);
|
||||
@@ -152,12 +157,12 @@ private Q_SLOTS:
|
||||
tap(makeKey(KeyType::Character, u"a"_s));
|
||||
tap(space);
|
||||
tap(space);
|
||||
QCOMPARE(engine.output, u"A ");
|
||||
QCOMPARE(engine.output, u"a ");
|
||||
}
|
||||
|
||||
void autoCapitalizationCanBeTurnedOff()
|
||||
{
|
||||
controller->resetContext();
|
||||
controller->resetContext(Field);
|
||||
controller->setOptions({.autoCapitalize = false});
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Off);
|
||||
tap(makeKey(KeyType::Character, u"a"_s));
|
||||
@@ -166,7 +171,7 @@ private Q_SLOTS:
|
||||
|
||||
void doubleTapShiftLocksAtSentenceStart()
|
||||
{
|
||||
controller->resetContext(); // a field was focused
|
||||
controller->resetContext(Field); // a field was focused
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once); // auto-shift
|
||||
const KeyDef shift = makeKey(KeyType::Shift);
|
||||
controller->keyDown(shift, 5000);
|
||||
@@ -281,7 +286,7 @@ private Q_SLOTS:
|
||||
|
||||
void autoShiftAndCapsLockDontApplyToFunctionKeys()
|
||||
{
|
||||
controller->resetContext(); // sentence start: auto-shift on
|
||||
controller->resetContext(Field); // sentence start: auto-shift on
|
||||
QCOMPARE(controller->shiftMode(), KeyboardController::ShiftMode::Once);
|
||||
tap(keysymKey(XKB_KEY_Left));
|
||||
QCOMPARE(engine.lastModifiers, Qt::NoModifier);
|
||||
@@ -303,10 +308,10 @@ private Q_SLOTS:
|
||||
lj.shifted = u"2"_s;
|
||||
KeyDef one = makeKey(KeyType::Character, u"+"_s);
|
||||
one.shifted = u"1"_s;
|
||||
controller->resetContext(); // sentence start
|
||||
controller->resetContext(Field); // sentence start
|
||||
tap(one);
|
||||
QCOMPARE(engine.output, u"+"); // not the shifted level
|
||||
controller->resetContext();
|
||||
controller->resetContext(Field);
|
||||
tap(lj);
|
||||
QCOMPARE(engine.output, u"+Ľ");
|
||||
tap(makeKey(KeyType::Shift)); // the user's shift does pick it
|
||||
@@ -325,6 +330,17 @@ private Q_SLOTS:
|
||||
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()
|
||||
{
|
||||
int dismissed = 0;
|
||||
|
||||
@@ -27,6 +27,18 @@ private Q_SLOTS:
|
||||
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()
|
||||
{
|
||||
VisibilityController v;
|
||||
|
||||
Reference in New Issue
Block a user