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
|
- 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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
: QObject(parent)
|
||||||
, QtWayland::zwp_input_method_context_v1(object)
|
, QtWayland::zwp_input_method_context_v1(object)
|
||||||
{
|
{
|
||||||
|
m_context.field = true;
|
||||||
announceModifiers();
|
announceModifiers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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); }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user