Reword code comments in plain technical language

Comments only, no code changes. Replaces vague or personified phrasing
with explicit statements of what the code does and why, uses bullet
lists for multi-case behavior, and fixes the settings path in
WindowSettings.h (~/.config/sloposk/sloposk.conf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-03 19:58:59 +01:00
co-authored by Claude Opus 5.5
parent 5e4d4051f3
commit 23e957660f
53 changed files with 232 additions and 226 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
// A small on/off button in the grab bar; lit while on. The icon goes inside // On/off button in the grab bar, highlighted while on.
// and should draw in `foreground`. // Child items form the icon and should use `foreground` as their color.
Item { Item {
id: toggle id: toggle
+3 -3
View File
@@ -1,11 +1,11 @@
import QtQuick import QtQuick
// The grab bar under the floating keyboard. Moving the window is up to the // Grab bar below the keyboard. Only emits dragStarted(); the parent starts
// owner (the window manager's move, so it works for mouse and touch alike). // the window manager's move (works for mouse and touch).
Item { Item {
id: handle id: handle
// Narrower when the bar's buttons leave less room in the middle. // Set smaller when the bar's buttons take up more width.
property real pillWidth: 120 property real pillWidth: 120
signal dragStarted() signal dragStarted()
+4 -4
View File
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
// The dismiss key left floating while the keyboard is shrunk; see-through so // Button shown in place of the keyboard while it is collapsed. Semi-transparent
// it hides less. Tap to bring the keyboard back; dragging is up to the owner // so it covers less. Tap emits tapped() (expand); drag emits dragStarted() and
// (the window manager's move). // the parent starts the window manager's move.
Item { Item {
id: button id: button
@@ -28,7 +28,7 @@ Item {
TapHandler { TapHandler {
id: tap id: tap
// The move ends with a release that would otherwise count as a tap. // Set during a drag, so the release that ends the drag isn't treated as a tap.
property bool moved: false property bool moved: false
onPressedChanged: if (pressed) moved = false onPressedChanged: if (pressed) moved = false
onTapped: if (!moved) button.tapped() onTapped: if (!moved) button.tapped()
+3 -3
View File
@@ -30,9 +30,9 @@ Item {
return modifierModes[modifier] ?? KeyboardController.Off return modifierModes[modifier] ?? KeyboardController.Off
return KeyboardController.Off return KeyboardController.Off
} }
// Shift shows locking with its glyph; the others turn accent-coloured. // Accent color for locked modifiers. Shift is excluded: its glyph shows the lock.
readonly property bool locked: latch === KeyboardController.Locked && keyType !== "shift" readonly property bool locked: latch === KeyboardController.Locked && keyType !== "shift"
// Character keys darken when pressed; function keys lighten (as on iPad). // Light background: character keys at rest, function keys while pressed or latched (as on iPad).
readonly property bool lit: (isCharacter !== pressed) || latch !== KeyboardController.Off readonly property bool lit: (isCharacter !== pressed) || latch !== KeyboardController.Off
readonly property var defaultGlyphs: ({ readonly property var defaultGlyphs: ({
@@ -46,7 +46,7 @@ Item {
return ["⇧", "⬆", "⇪"][shiftMode] return ["⇧", "⬆", "⇪"][shiftMode]
return label !== "" ? label : (defaultGlyphs[keyType] ?? "") return label !== "" ? label : (defaultGlyphs[keyType] ?? "")
} }
// Count code points, not UTF-16 units, so emoji glyphs aren't treated as words. // Multi-character label (e.g. "return"). Counts code points, not UTF-16 units, so an emoji counts as one.
readonly property bool isWord: [...displayLabel].length > 1 && !isCharacter readonly property bool isWord: [...displayLabel].length > 1 && !isCharacter
x: keyX x: keyX
+9 -9
View File
@@ -12,36 +12,36 @@ Window {
property var collapse: null // CollapseController, set from C++ property var collapse: null // CollapseController, set from C++
readonly property bool collapsed: collapse?.collapsed ?? false readonly property bool collapsed: collapse?.collapsed ?? false
property bool settingsOpen: false property bool settingsOpen: false
// Height of the key area, for C++ keeping key sizes when rows change. // Read by C++ to keep key size constant when rows are added or removed.
readonly property real keysHeight: keyboard.height readonly property real keysHeight: keyboard.height
// Transparent room above the keyboard for long-press popups on the top row. // Transparent space above the keyboard for the top row's long-press popups.
readonly property real popupMargin: 90 readonly property real popupMargin: 90
// Outside this, input passes through to whatever is below the window. // Input outside this rect goes to the window underneath.
readonly property rect inputRegion: collapsed readonly property rect inputRegion: collapsed
? Qt.rect(floatingButton.x, floatingButton.y, floatingButton.width, floatingButton.height) ? Qt.rect(floatingButton.x, floatingButton.y, floatingButton.width, floatingButton.height)
: keyboard.alternates.open : keyboard.alternates.open
? Qt.rect(0, 0, width, height) ? Qt.rect(0, 0, width, height)
: Qt.rect(panel.x, panel.y, panel.width, panel.height) : Qt.rect(panel.x, panel.y, panel.width, panel.height)
// Empty when nothing shows through, so the compositor skips the blur. // Empty (no blur) when collapsed, blur is off, or the background is opaque.
readonly property rect blurRegion: !collapsed && settings.blurBackground && settings.backgroundOpacity < 1 readonly property rect blurRegion: !collapsed && settings.blurBackground && settings.backgroundOpacity < 1
? Qt.rect(panel.x, panel.y, panel.width, panel.height) ? Qt.rect(panel.x, panel.y, panel.width, panel.height)
: Qt.rect(0, 0, 0, 0) : Qt.rect(0, 0, 0, 0)
width: 1000 width: 1000
height: 340 + popupMargin height: 340 + popupMargin
// None while shrunk to the floating button. // No minimum while collapsed, so the window can shrink to the button.
minimumWidth: collapsed ? 0 : 480 minimumWidth: collapsed ? 0 : 480
minimumHeight: collapsed ? 0 : 180 + popupMargin minimumHeight: collapsed ? 0 : 180 + popupMargin
visible: false visible: false
color: "transparent" color: "transparent"
// Alt+F4 or the task manager would hide it behind the visibility // Ignore close requests (Alt+F4, task manager): they would hide the window
// controller's back; the dismiss key and the tray toggle hide it instead. // without VisibilityController knowing. Hiding goes through the dismiss key or tray toggle.
onClosing: close => close.accepted = false onClosing: close => close.accepted = false
onInputRegionChanged: placement?.setInputRegion(inputRegion) onInputRegionChanged: placement?.setInputRegion(inputRegion)
onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius) onBlurRegionChanged: placement?.setBlurRegion(blurRegion, Theme.windowRadius)
// For CollapseController. // Called by CollapseController.
function dismissKeyRect() { function dismissKeyRect() {
return keyboard.keyRect("dismiss"); return keyboard.keyRect("dismiss");
} }
@@ -107,7 +107,7 @@ Window {
FloatingButton { FloatingButton {
id: floatingButton id: floatingButton
readonly property rect keyRect: root.collapse?.keyRect ?? Qt.rect(0, 0, 0, 0) readonly property rect keyRect: root.collapse?.keyRect ?? Qt.rect(0, 0, 0, 0)
// Over the key while the window is full size, then in the shrunk window. // keyRect is in full-window coordinates; the clamp keeps the button inside the window once it has shrunk.
x: Math.max(0, Math.min(keyRect.x, root.width - width)) x: Math.max(0, Math.min(keyRect.x, root.width - width))
y: Math.max(0, Math.min(keyRect.y, root.height - height)) y: Math.max(0, Math.min(keyRect.y, root.height - height))
width: keyRect.width width: keyRect.width
+8 -8
View File
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import Sloposk import Sloposk
// One-tap switches for the most changed key options, beside the gear. // Toggle buttons for frequently changed key options, next to the settings button.
// Icons are drawn from boxes so they don't depend on the fonts installed. // Icons are built from Rectangles, so they don't depend on installed fonts.
Row { Row {
id: toggles id: toggles
@@ -13,8 +13,8 @@ Row {
return layoutModel.sections.some(s => s.id === id); return layoutModel.sections.some(s => s.id === id);
} }
// Shows or hides one of the template's optional sections; absent from // Shows or hides an optional template section. Not displayed if the
// layouts without it. // layout has no such section.
component SectionToggle: BarToggle { component SectionToggle: BarToggle {
required property string section required property string section
@@ -23,7 +23,7 @@ Row {
onToggled: checked => toggles.settings.setSectionVisible(section, checked) onToggled: checked => toggles.settings.setSectionVisible(section, checked)
} }
// A grid of tiny keys, optionally in a keyboard outline. // Icon: grid of small keys, optionally inside a keyboard outline.
component KeyGrid: Item { component KeyGrid: Item {
id: keyGrid id: keyGrid
required property color color required property color color
@@ -58,7 +58,7 @@ Row {
} }
} }
// A key outline with a label on it. // Icon: key outline with a text label.
component LabelledKey: Rectangle { component LabelledKey: Rectangle {
property alias label: caption.text property alias label: caption.text
property alias textColor: caption.color property alias textColor: caption.color
@@ -90,7 +90,7 @@ Row {
label: qsTr("Hold Keys") label: qsTr("Hold Keys")
checked: toggles.settings.holdKeys checked: toggles.settings.holdKeys
onToggled: checked => toggles.settings.holdKeys = checked onToggled: checked => toggles.settings.holdKeys = checked
// A fingertip resting on a key, holding it down. // Icon: finger pressing a key (hold-keys mode).
Rectangle { Rectangle {
anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom } anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom }
width: 18 width: 18
@@ -119,7 +119,7 @@ Row {
id: navigation id: navigation
section: "navigation" section: "navigation"
label: qsTr("Navigation Keys") label: qsTr("Navigation Keys")
// The inverted T of the arrow keys. // Icon: arrow keys in an inverted T.
Repeater { Repeater {
model: [[1, 0], [0, 1], [1, 1], [2, 1]] model: [[1, 0], [0, 1], [1, 1], [2, 1]]
Rectangle { Rectangle {
+2 -2
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
// Corner grip. Resizing is up to the owner (the window manager's resize). // Corner grip. Only emits dragStarted(); the parent starts the window manager's resize.
Item { Item {
id: grip id: grip
@@ -28,7 +28,7 @@ Item {
DragHandler { DragHandler {
target: null target: null
// The grab bar lies underneath and would take the drag over. // Prevent the grab bar underneath from taking over the drag.
grabPermissions: PointerHandler.TakeOverForbidden grabPermissions: PointerHandler.TakeOverForbidden
cursorShape: Qt.SizeFDiagCursor cursorShape: Qt.SizeFDiagCursor
onActiveChanged: if (active) grip.dragStarted() onActiveChanged: if (active) grip.dragStarted()
+1 -1
View File
@@ -5,7 +5,7 @@ Item {
id: button id: button
property bool checked: false property bool checked: false
// Something doesn't work (see Diagnostics): a dot invites a look in settings. // Shows a dot when Diagnostics reports a problem.
property bool alert: false property bool alert: false
signal clicked() signal clicked()
+3 -3
View File
@@ -1,8 +1,8 @@
import QtQuick import QtQuick
import Sloposk import Sloposk
// Replaces the keys while open, over the window's own background so // Shown instead of the keys while open. Drawn on the window background, so
// opacity changes show live. Binds straight to Settings, which persists. // opacity changes are visible immediately. Bound directly to Settings, which saves to disk.
Flickable { Flickable {
id: panel id: panel
@@ -113,7 +113,7 @@ Flickable {
} }
Repeater { Repeater {
// "main" is the typing keys; the rest come from the template. // "main" = the typing keys; other ids are template sections.
model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections) model: [{ id: "main", name: qsTr("Typing keys") }].concat(panel.layoutModel.sections)
ToggleRow { ToggleRow {
+1 -1
View File
@@ -1,6 +1,6 @@
import QtQuick import QtQuick
// A labelled slider in the iOS style; value runs from `from` to 1. // iOS-style labelled slider; value range is `from`..1.
Item { Item {
id: row id: row
+2 -2
View File
@@ -28,8 +28,8 @@ QString fakeInputProblem(const KWinFakeInput &fakeInput)
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin) void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin)
{ {
// Qt binds globals KWin already announced on construction, so the first // Qt binds the globals KWin has already announced during construction, so
// check is final unless KWin restarts. // this result only changes if KWin restarts.
const auto checkInputMethod = [&] { const auto checkInputMethod = [&] {
diagnostics.check(QStringLiteral("input-method"), launchedByKWin && backend.isAvailable(), diagnostics.check(QStringLiteral("input-method"), launchedByKWin && backend.isAvailable(),
tr("Not running as KWin's virtual keyboard, so it can't type into text fields: " tr("Not running as KWin's virtual keyboard, so it can't type into text fields: "
+4 -5
View File
@@ -4,11 +4,10 @@ class Diagnostics;
class KWinInputMethod; class KWinInputMethod;
class SystemLayouts; class SystemLayouts;
// Reports to `diagnostics` what this session doesn't allow, and keeps the // Reports missing protocols and D-Bus services to `diagnostics`, and updates
// reports current as protocols and services come and go. // the reports when they appear or disappear.
// KWin offers the input-method global to every client but only activates // `launchedByKWin`: WAYLAND_SOCKET was set at startup. KWin advertises the
// the process it launched, which it hands a connection: `launchedByKWin` is // input-method global to every client, but only activates the process it launched.
// whether WAYLAND_SOCKET was set at startup.
void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin); void watchInputMethod(Diagnostics &diagnostics, const KWinInputMethod &backend, bool launchedByKWin);
void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts); void watchSystemLayouts(Diagnostics &diagnostics, const SystemLayouts &layouts);
void reportDBusControl(Diagnostics &diagnostics, bool registered); void reportDBusControl(Diagnostics &diagnostics, bool registered);
+9 -7
View File
@@ -4,10 +4,10 @@
#include <functional> #include <functional>
// KWin's virtual keyboard policy (the tray toggle, System Settings → // Queries KWin's virtual keyboard policy (tray toggle, System Settings →
// Virtual Keyboard): KWin activates the input method for every text field // Virtual Keyboard) over org.kde.KWin /VirtualKeyboard.
// and the policy only gates its input panel, which sloposk doesn't use, so // Needed because KWin activates the input method for every text field and
// sloposk asks it over org.kde.KWin /VirtualKeyboard instead. // applies the policy only to the input panel surface, which sloposk doesn't use.
class KWinVirtualKeyboard : public QObject class KWinVirtualKeyboard : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -15,9 +15,11 @@ class KWinVirtualKeyboard : public QObject
public: public:
explicit KWinVirtualKeyboard(QObject *parent = nullptr); explicit KWinVirtualKeyboard(QObject *parent = nullptr);
// Calls `answer` with whether a field focused now should show the // Asynchronously asks KWin whether to show the keyboard for the currently
// keyboard: never when it's off, only after touch or a pen in the // focused field and calls `answer` with the result:
// default mode. True if KWin can't be asked. // - false if the virtual keyboard is disabled
// - in the default (touch-only) mode, true only if the last input was touch or pen
// - true if KWin can't be reached
void askShowOnActive(QObject *context, std::function<void(bool)> answer); void askShowOnActive(QObject *context, std::function<void(bool)> answer);
Q_SIGNALS: Q_SIGNALS:
+1 -1
View File
@@ -23,7 +23,7 @@ SystemLayouts::SystemLayouts(QObject *parent)
: QObject(parent) : QObject(parent)
{ {
QDBusConnection bus = QDBusConnection::sessionBus(); QDBusConnection bus = QDBusConnection::sessionBus();
// Follows the service's owner, so it survives KWin restarting. // Watches the service owner, so it reconnects after KWin restarts.
bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint))); bus.connect(Service, Path, Interface, QStringLiteral("layoutChanged"), this, SLOT(setCurrent(uint)));
auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this); auto *watcher = new QDBusServiceWatcher(Service, bus, QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &SystemLayouts::fetchCurrent); connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &SystemLayouts::fetchCurrent);
+5 -5
View File
@@ -2,10 +2,10 @@
#include <QObject> #include <QObject>
// Which of the system's keyboard layouts is active, followed and switched // Tracks and switches the active system keyboard layout via KWin's
// over KWin's org.kde.keyboard D-Bus interface, so the on-screen and the // org.kde.keyboard D-Bus interface, so the on-screen and physical keyboards
// physical keyboard always agree. Without that service it switches locally, // use the same layout. While the service is missing (KWin not started yet or
// until the service appears (KWin starting late or restarting). // restarting), switching only changes the local index.
class SystemLayouts : public QObject class SystemLayouts : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -19,7 +19,7 @@ public:
Q_SIGNALS: Q_SIGNALS:
void currentChanged(); void currentChanged();
// After each attempt to reach the service, and when it goes away. // Emitted after each attempt to reach the service, and when it disappears.
void serviceChecked(bool available); void serviceChecked(bool available);
private Q_SLOTS: private Q_SLOTS:
+3 -3
View File
@@ -5,8 +5,8 @@
#include "qwayland-fake-input.h" #include "qwayland-fake-input.h"
// Key events for whatever window has focus, as from a hardware keyboard: // Sends key events to the focused window, as a hardware keyboard would, via
// KWin's fake-input protocol, which it offers to the input-method process. // KWin's fake-input protocol (offered to the input-method process).
// Used when no text field is focused, so shortcuts, terminals and games work. // 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 class KWinFakeInput : public QWaylandClientExtensionTemplate<KWinFakeInput>, public QtWayland::org_kde_kwin_fake_input
{ {
@@ -26,7 +26,7 @@ public:
void typeText(const QString &text); void typeText(const QString &text);
// KWin offers the protocol only to clients whose desktop file lists it // KWin offers the protocol only to clients whose desktop file lists it
// (see the .desktop.in); isActive() says whether it did. // (see the .desktop.in); isActive() says whether it did.
// keyboard_keysym, which everything here uses, needs this version. // Minimum version: required for keyboard_keysym, which all requests here use.
static constexpr uint32_t RequiredVersion = 6; static constexpr uint32_t RequiredVersion = 6;
bool canSendKeysyms() const; bool canSendKeysyms() const;
// The version KWin bound, which may be older than the one asked for: // The version KWin bound, which may be older than the one asked for:
+1 -1
View File
@@ -58,7 +58,7 @@ void KWinInputMethod::replaceBeforeCursor(const QString &before, const QString &
m_context->replaceBeforeCursor(before, text); m_context->replaceBeforeCursor(before, text);
return; return;
} }
// Key events are one ordered stream, so plain Backspaces are safe here. // Key events are delivered in order, so sending Backspace keys is safe here.
for (qsizetype i = 0; i < before.toUcs4().size(); ++i) for (qsizetype i = 0; i < before.toUcs4().size(); ++i)
m_fakeInput.sendKeysym(XKB_KEY_BackSpace); m_fakeInput.sendKeysym(XKB_KEY_BackSpace);
m_fakeInput.typeText(text); m_fakeInput.typeText(text);
+3 -2
View File
@@ -42,7 +42,7 @@ public:
// False when the compositor didn't hand us the input-method global. // False when the compositor didn't hand us the input-method global.
bool isAvailable() const; bool isAvailable() const;
// Key events outside text fields, held keys and modifier combos. // Used for key events outside text fields, held keys and modifier combos.
const KWinFakeInput &fakeInput() const { return m_fakeInput; } const KWinFakeInput &fakeInput() const { return m_fakeInput; }
bool isActive() const override; bool isActive() const override;
@@ -64,6 +64,7 @@ private:
KWinInputMethodGlobal m_global; KWinInputMethodGlobal m_global;
KWinFakeInput m_fakeInput; KWinFakeInput m_fakeInput;
std::unique_ptr<KWinInputContext> m_context; std::unique_ptr<KWinInputContext> m_context;
// Replaced by an activate before KWin deactivated it; kept until it does. // Context replaced by a new activate before KWin deactivated it. Kept
// until KWin sends its deactivate, then destroyed.
std::unique_ptr<KWinInputContext> m_previous; std::unique_ptr<KWinInputContext> m_previous;
}; };
+1 -1
View File
@@ -34,7 +34,7 @@ public:
virtual void setKeyPressed(quint32 keysym, bool pressed) = 0; virtual void setKeyPressed(quint32 keysym, bool pressed) = 0;
Q_SIGNALS: Q_SIGNALS:
// Also when focus moves straight from one field to another. // Also emitted when focus moves directly from one field to another.
void activeChanged(); void activeChanged();
void contextChanged(); void contextChanged();
}; };
+4 -3
View File
@@ -29,7 +29,8 @@ struct TextContext {
Purpose purpose = Purpose::Normal; Purpose purpose = Purpose::Normal;
quint32 hints = 0; quint32 hints = 0;
// Prose fields get iOS conveniences; codes, addresses and secrets don't. // Whether typing aids (auto-capitalization, double-space period) apply:
// yes for normal text and names, no for passwords, URLs, emails, numbers etc.
bool isProse() const bool isProse() const
{ {
const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha const bool prosePurpose = purpose == Purpose::Normal || purpose == Purpose::Alpha
@@ -37,8 +38,8 @@ struct TextContext {
return field && prosePurpose && !(hints & (HiddenText | SensitiveData)); return field && prosePurpose && !(hints & (HiddenText | SensitiveData));
} }
// Fields that state any text preference must ask for capitals; most that // Fields that set any text hint get auto-capitalization only if they set
// state none (hints 0) still want them. // AutoCapitalization. Fields with no hints get it by default.
bool allowsAutoCapitalization() const bool allowsAutoCapitalization() const
{ {
constexpr quint32 Preferences = AutoCompletion | AutoCorrection | AutoCapitalization | Lowercase constexpr quint32 Preferences = AutoCompletion | AutoCorrection | AutoCapitalization | Lowercase
+5 -4
View File
@@ -26,10 +26,11 @@ public:
int selected() const { return m_selected; } int selected() const { return m_selected; }
QString selectedText() const { return m_choices.value(m_selected); } QString selectedText() const { return m_choices.value(m_selected); }
// Places one key-sized cell per choice just above the key, starting over it // Lays out one key-sized cell per choice in a row above the key, starting
// and extending right (shifted left near the edge to stay within // at the key's x and extending right.
// [0, boundsWidth]; lowered, over the key, to stay below minTop). The cell // - shifted left if needed to stay within [0, boundsWidth]
// above the finger starts selected. // - moved down (overlapping the key) if needed to stay below minTop
// Selects the cell above the finger.
void open(const QRectF &keyRect, const QStringList &choices, qreal boundsWidth, qreal minTop, qreal gap); void open(const QRectF &keyRect, const QStringList &choices, qreal boundsWidth, qreal minTop, qreal gap);
void selectAt(qreal x); void selectAt(qreal x);
void close(); void close();
+2 -1
View File
@@ -51,7 +51,8 @@ QString DeadKeys::press(quint32 deadKeysym)
return composed; return composed;
} }
case XKB_COMPOSE_CANCELLED: { case XKB_COMPOSE_CANCELLED: {
// No sequence goes on like this: type the old accents, start anew. // No compose sequence continues with this key: flush the pending accents
// and start a new sequence.
const QString accents = m_pending; const QString accents = m_pending;
cancel(); cancel();
return accents + press(deadKeysym); return accents + press(deadKeysym);
+6 -5
View File
@@ -17,12 +17,13 @@ public:
~DeadKeys(); ~DeadKeys();
bool isPending() const { return !m_pending.isEmpty(); } bool isPending() const { return !m_pending.isEmpty(); }
// A dead key was typed: starts or extends the sequence. Returns text to // Dead key pressed: starts or extends the sequence. Returns text to type now:
// type now: what two dead keys composed (´´ → ´), or the accents of a // - the result if the dead keys composed (´´ → ´)
// sequence the new one can't continue; empty while still pending. // - the pending accents if this key can't continue the sequence
// - empty while the sequence is still pending
QString press(quint32 deadKeysym); QString press(quint32 deadKeysym);
// `text` typed while pending: the composed character, or the accents // Text key pressed while a sequence is pending. Returns the composed
// followed by `text` when nothing composes. Ends the sequence. // character, or the pending accents + `text` if nothing composes. Ends the sequence.
QString compose(const QString &text); QString compose(const QString &text);
void cancel(); void cancel();
+7 -6
View File
@@ -7,11 +7,12 @@
class InputEngine; class InputEngine;
// The "hold keys" mode: the keyboard acts like a hardware one. A key is down // "Hold keys" mode: behaves like a hardware keyboard.
// while the finger is, so apps see held keys (games, Shift+click, the // - a key is pressed while touched and released on lift, so apps see held
// system's key repeat). Shift, Ctrl, Alt, Super and AltGr are plain keys, caps lock // keys (games, Shift+click, system key repeat)
// is the system's, and dead keys compose in the app; none of the touch // - Shift, Ctrl, Alt, Super and AltGr are normal keys; caps lock is the system's
// conveniences (sticky shift, auto-capitalization, accents) apply. // - dead keys are sent as-is and composed by the app
// - no sticky modifiers, auto-capitalization or long-press accents
class HardwareKeys class HardwareKeys
{ {
public: public:
@@ -25,7 +26,7 @@ public:
// Lets go of everything still down, e.g. when the mode is turned off. // Lets go of everything still down, e.g. when the mode is turned off.
void releaseAll(); void releaseAll();
// For the view: shift and AltGr shown while held, caps lock while on. // For the view: shift/AltGr are on while held, caps lock while active.
KeyLevel keyLevel() const; KeyLevel keyLevel() const;
bool isShiftHeld() const; bool isShiftHeld() const;
bool isCapsLockOn() const { return m_capsLock; } bool isCapsLockOn() const { return m_capsLock; }
+10 -10
View File
@@ -14,10 +14,10 @@ static_assert(int(KeyboardController::ShiftMode::Locked) == int(ShiftState::Mode
namespace { namespace {
// Enough history for the text rules; fields may report megabytes. // Enough history for the text rules; fields may report megabytes.
constexpr qsizetype MaxTextBefore = 256; constexpr qsizetype MaxTextBefore = 256;
// Stands for text we can't see: neither a sentence start nor a word, so no // Placeholder for unknown text before the cursor (field doesn't report text,
// rule fires on it. For fields that don't report text, after cursor moves. // or the cursor moved). Neither a sentence start nor a word, so no text rule triggers.
constexpr QChar UnknownText = u'\uFFFC'; constexpr QChar UnknownText = u'\uFFFC';
// Like a hardware key: no word mode, a faster rate than Backspace. // Same as a hardware key repeat: no word mode, faster than Backspace.
constexpr KeyRepeater::Timing KeysymRepeat{.intervalMs = 50, .wordModeAfterMs = std::numeric_limits<int>::max()}; constexpr KeyRepeater::Timing KeysymRepeat{.intervalMs = 50, .wordModeAfterMs = std::numeric_limits<int>::max()};
} }
@@ -72,7 +72,7 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
break; break;
case KeyType::Backspace: case KeyType::Backspace:
if (m_deadKeys.isPending()) { if (m_deadKeys.isPending()) {
m_deadKeys.cancel(); // backspace takes back the accent m_deadKeys.cancel(); // backspace discards the pending accent
} else if (m_modifiers.active()) { } else if (m_modifiers.active()) {
sendKey(XKB_KEY_BackSpace); sendKey(XKB_KEY_BackSpace);
} else { } else {
@@ -156,7 +156,7 @@ void KeyboardController::keyCancelled(const KeyDef &key)
void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs) void KeyboardController::slideReleased(const KeyDef &target, qint64 timeMs)
{ {
holdKey(target, true); // only in hold-keys mode: the slide never pressed it holdKey(target, true); // hold-keys mode only: the slide target was never pressed, press it now
keyUp(target, KeyOutput::Primary, {}, timeMs); keyUp(target, KeyOutput::Primary, {}, timeMs);
m_model.setLayer(m_layerBeforeSwitch); m_model.setLayer(m_layerBeforeSwitch);
} }
@@ -190,7 +190,7 @@ void KeyboardController::setContext(const TextContext &context)
if (context.hasSurroundingText) { if (context.hasSurroundingText) {
m_textBefore = context.textBeforeCursor.right(MaxTextBefore); m_textBefore = context.textBeforeCursor.right(MaxTextBefore);
} else if (m_context.hasSurroundingText) { } else if (m_context.hasSurroundingText) {
// The field stopped reporting (an input-method reset): what we knew is stale. // The field stopped reporting text (input-method reset): drop the tracked text.
m_deadKeys.cancel(); m_deadKeys.cancel();
m_textBefore.clear(); m_textBefore.clear();
} }
@@ -220,8 +220,8 @@ void KeyboardController::typeCharacter(const KeyDef &key, KeyOutput output, cons
{ {
const QString base = key.type == KeyType::Space ? QStringLiteral(" ") : key.label; const QString base = key.type == KeyType::Space ? QStringLiteral(" ") : key.label;
if (m_modifiers.active()) { if (m_modifiers.active()) {
// A shortcut such as Ctrl+C: the key's unshifted symbol, with modifiers; // Shortcut such as Ctrl+C: send the key's unshifted keysym with modifiers
// for a dead key its dead keysym, as the physical key sends. // (for a dead key, its dead keysym, same as the physical key).
m_deadKeys.cancel(); m_deadKeys.cancel();
sendKey(key.dead ? key.dead : xkb_utf32_to_keysym(base.toLower().toUcs4().value(0))); sendKey(key.dead ? key.dead : xkb_utf32_to_keysym(base.toLower().toUcs4().value(0)));
return; return;
@@ -229,7 +229,7 @@ void KeyboardController::typeCharacter(const KeyDef &key, KeyOutput output, cons
const KeyLevel level = keyLevel(); const KeyLevel level = keyLevel();
if (level.altGr) { if (level.altGr) {
// Used up by this key, like a one-shot shift. // Clear one-shot modifiers, like a one-shot shift.
m_modifiers.keyTyped(); m_modifiers.keyTyped();
modifiersUpdated(); modifiersUpdated();
} }
@@ -295,7 +295,7 @@ void KeyboardController::deleteBackward(bool wholeWord)
autoCapitalize(); autoCapitalize();
} }
// Only the key that started the repeat stops it; another finger's key doesn't. // Only releasing the key that started the repeat stops it, not another finger's key.
void KeyboardController::stopRepeating(const KeyDef &key) void KeyboardController::stopRepeating(const KeyDef &key)
{ {
if (key.keysym == m_repeatingKeysym) if (key.keysym == m_repeatingKeysym)
+20 -20
View File
@@ -19,15 +19,15 @@ class LayoutModel;
// Which of a key's outputs the gesture selected. // Which of a key's outputs the gesture selected.
enum class KeyOutput { Primary, Flick, Alternate }; enum class KeyOutput { Primary, Flick, Alternate };
// Keyboard rules: what each key does, shift and the other modifiers, dead // Keyboard logic: key actions, shift and other modifiers, dead keys, layers
// keys, layers and the iOS conveniences (auto-capitalization, double-space // and iOS-style typing aids (auto-capitalization, double-space period,
// period, backspace repeat). Knows nothing about touch or Wayland: it // backspace repeat). Independent of touch and Wayland: input is key presses
// receives key presses and text-field context, and talks to an InputEngine. // and text-field context, output goes to an InputEngine.
// //
// With Ctrl, Alt or Super active, keys become shortcuts (keysyms with those // With Ctrl, Alt or Super active, keys send shortcuts (keysym + modifiers)
// modifiers) instead of text; AltGr instead types a key's AltGr symbol. Shift joins shortcuts and non-text keys // instead of text. AltGr types the key's AltGr symbol.
// (Shift+F5, Shift+Tab, Shift+arrows) only when the user turned it on, not // Shift is added to shortcuts and non-text keys (Shift+F5, Shift+Tab,
// when auto-capitalization did. // Shift+arrows) only if the user enabled it, not if auto-capitalization did.
class KeyboardController : public QObject class KeyboardController : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -45,19 +45,19 @@ public:
ShiftMode shiftMode() const; ShiftMode shiftMode() const;
QVariantMap modifierModes() const; QVariantMap modifierModes() const;
// Hold-keys mode: no long-press, flick or trackpad gestures, no rollover. // Hold-keys mode disables long-press, flick and trackpad gestures, and rollover.
bool holdsKeys() const { return m_options.holdKeys; } bool holdsKeys() const { return m_options.holdKeys; }
void keyDown(const KeyDef &key, qint64 timeMs); void keyDown(const KeyDef &key, qint64 timeMs);
void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs); void keyUp(const KeyDef &key, KeyOutput output, const QString &alternate, qint64 timeMs);
void keyCancelled(const KeyDef &key); void keyCancelled(const KeyDef &key);
// Finger slid from a layer key onto `target`: type it, then switch back. // Finger slid from a layer key onto `target` and lifted: type it, then switch back to the previous layer.
void slideReleased(const KeyDef &target, qint64 timeMs); void slideReleased(const KeyDef &target, qint64 timeMs);
// 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 (context.field) or unfocused: forget what we knew // Field focused (context.field) or unfocused: discard tracked text and
// about the text and start over from what it reported. // start from `context`.
void resetContext(const TextContext &context); 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);
@@ -67,22 +67,22 @@ Q_SIGNALS:
void shiftChanged(); void shiftChanged();
void modifiersChanged(); void modifiersChanged();
void dismissRequested(); void dismissRequested();
void nextLayoutRequested(); // the globe key: switching is the system's job void nextLayoutRequested(); // globe key; SystemLayouts does the switch
// Hold-keys mode is about to flip: keys still down must be cancelled now, // Hold-keys mode is about to change: release keys still down now, using
// under the rules they were pressed with. // the mode they were pressed in.
void holdKeysAboutToChange(); void holdKeysAboutToChange();
private: private:
void typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate); void typeCharacter(const KeyDef &key, KeyOutput output, const QString &alternate);
void typeText(const QString &text); void typeText(const QString &text);
// Sends a non-text key or shortcut with the active modifiers, then uses them up. // Sends a non-text key or shortcut with the active modifiers, then clears one-shot modifiers.
void sendKey(quint32 keysym); void sendKey(quint32 keysym);
Qt::KeyboardModifiers shortcutModifiers() const; Qt::KeyboardModifiers shortcutModifiers() const;
KeyLevel keyLevel() const; KeyLevel keyLevel() const;
// Hold-keys mode: forwards the key to m_hardware; false if it isn't one it presses. // Hold-keys mode: forwards the key to m_hardware. Returns false if m_hardware doesn't handle it.
bool holdKey(const KeyDef &key, bool pressed); bool holdKey(const KeyDef &key, bool pressed);
void useModifiers(); void useModifiers();
// The modifiers changed: refreshes the labels and tells the view. // Call after modifiers change: updates key labels and notifies the view.
void modifiersUpdated(); void modifiersUpdated();
void deleteBackward(bool wholeWord); void deleteBackward(bool wholeWord);
void sendRepeatedKeysym(); void sendRepeatedKeysym();
@@ -105,7 +105,7 @@ private:
QString m_layerBeforeSwitch; QString m_layerBeforeSwitch;
TextContext m_context; TextContext m_context;
// Best guess of the text before the cursor: the field's last report plus // Estimated text before the cursor: the field's last report plus what we
// what we typed since. Fields that don't report text rely on the latter. // typed since. For fields that don't report text, only what we typed.
QString m_textBefore; QString m_textBefore;
}; };
+8 -8
View File
@@ -104,13 +104,13 @@ void KeyboardSurface::pointerDown(int id, QPointF pos, qint64 timeMs)
m_lastEventMs = timeMs; m_lastEventMs = timeMs;
if (!m_model || !m_controller) if (!m_model || !m_controller)
return; return;
// A press for an id still down means its release was lost. // A press for an id that is already down means its release was lost.
if (const auto stale = m_pointers.constFind(id); stale != m_pointers.cend()) { if (const auto stale = m_pointers.constFind(id); stale != m_pointers.cend()) {
const Pointer pointer = *stale; const Pointer pointer = *stale;
m_pointers.erase(stale); m_pointers.erase(stale);
cancel(pointer); cancel(pointer);
} }
// Rollover would let go of keys still held, which hold-keys mode must not. // No rollover in hold-keys mode: it would release keys that are still held.
if (!m_controller->holdsKeys()) if (!m_controller->holdsKeys())
commitPendingTaps(timeMs); commitPendingTaps(timeMs);
@@ -179,7 +179,7 @@ void KeyboardSurface::longPressElapsed(int id, quint64 serial)
if (it->tracker.gesture() == Gesture::Trackpad) if (it->tracker.gesture() == Gesture::Trackpad)
setTrackpadActive(true); setTrackpadActive(true);
else if (it->tracker.gesture() == Gesture::LongPress) else if (it->tracker.gesture() == Gesture::LongPress)
openAlternates(Pointer(*it), m_lastEventMs); // a copy: opening may erase from m_pointers openAlternates(Pointer(*it), m_lastEventMs); // copy: openAlternates() may erase from m_pointers
} }
// --- Gesture outcomes // --- Gesture outcomes
@@ -198,7 +198,7 @@ void KeyboardSurface::finish(const Pointer &pointer, qint64 timeMs)
m_controller->keyUp(key, tracker.flickCommitted() ? KeyOutput::Flick : KeyOutput::Primary, {}, timeMs); m_controller->keyUp(key, tracker.flickCommitted() ? KeyOutput::Flick : KeyOutput::Primary, {}, timeMs);
break; break;
case Gesture::LongPress: { case Gesture::LongPress: {
// Without the popup (it never opened), the key types its primary output. // Popup never opened: type the key's primary output.
const QString alternate = ownsAlternates(pointer) ? m_alternates.selectedText() : QString(); const QString alternate = ownsAlternates(pointer) ? m_alternates.selectedText() : QString();
if (ownsAlternates(pointer)) if (ownsAlternates(pointer))
closeAlternates(); closeAlternates();
@@ -230,7 +230,7 @@ void KeyboardSurface::cancel(const Pointer &pointer)
m_controller->keyCancelled(pointer.tracker.key()); m_controller->keyCancelled(pointer.tracker.key());
} }
// Clears the visual state a pointer left on the model. // Clears the pointer's pressed/hover state in the model.
void KeyboardSurface::release(const Pointer &pointer) void KeyboardSurface::release(const Pointer &pointer)
{ {
m_model->setPressed(pointer.index, false); m_model->setPressed(pointer.index, false);
@@ -261,12 +261,12 @@ void KeyboardSurface::updateSlideHover(Pointer &pointer)
pointer.hoverIndex = hover; pointer.hoverIndex = hover;
} }
// One popup: a second long-press first finishes the one showing it, as a new // Only one popup at a time: a second long-press first commits the open popup,
// press commits pending taps. // the same way a new press commits pending taps.
void KeyboardSurface::openAlternates(const Pointer &pointer, qint64 timeMs) void KeyboardSurface::openAlternates(const Pointer &pointer, qint64 timeMs)
{ {
if (pointer.index < 0) if (pointer.index < 0)
return; // the layout changed under the finger: no key to place it over return; // layer changed during the press: the key is gone
for (auto it = m_pointers.begin(); it != m_pointers.end(); ++it) { for (auto it = m_pointers.begin(); it != m_pointers.end(); ++it) {
if (it->serial != pointer.serial && ownsAlternates(*it)) { if (it->serial != pointer.serial && ownsAlternates(*it)) {
const Pointer owner = *it; const Pointer owner = *it;
+8 -9
View File
@@ -10,10 +10,9 @@
#include <QQuickItem> #include <QQuickItem>
#include <limits> #include <limits>
// Receives all touch points (and the mouse) over the keyboard, runs one // Handles touch points and the mouse over the keyboard. Runs one
// PointerTracker per finger, and turns finished gestures into controller // PointerTracker per finger and turns finished gestures into controller calls.
// calls. Several fingers can type at once: a new press commits pending taps // Rollover (as on iOS): a new press commits the other fingers' pending taps.
// (rollover), as on iOS.
class KeyboardSurface : public QQuickItem class KeyboardSurface : public QQuickItem
{ {
Q_OBJECT Q_OBJECT
@@ -22,8 +21,8 @@ class KeyboardSurface : public QQuickItem
Q_PROPERTY(KeyboardController *controller READ controller WRITE setController NOTIFY controllerChanged) Q_PROPERTY(KeyboardController *controller READ controller WRITE setController NOTIFY controllerChanged)
Q_PROPERTY(AlternatesPicker *alternates READ alternates CONSTANT) Q_PROPERTY(AlternatesPicker *alternates READ alternates CONSTANT)
Q_PROPERTY(bool trackpadActive READ isTrackpadActive NOTIFY trackpadActiveChanged) Q_PROPERTY(bool trackpadActive READ isTrackpadActive NOTIFY trackpadActiveChanged)
// Room above the surface that long-press popups may use; taller keys put // Space above the surface available to long-press popups. If a popup
// top-row popups lower, over their key, instead of cutting them off. // doesn't fit, it is moved down over its key instead of being clipped.
Q_PROPERTY(qreal popupHeadroom MEMBER m_popupHeadroom NOTIFY popupHeadroomChanged) Q_PROPERTY(qreal popupHeadroom MEMBER m_popupHeadroom NOTIFY popupHeadroomChanged)
public: public:
@@ -53,10 +52,10 @@ protected:
private: private:
struct Pointer { struct Pointer {
PointerTracker tracker; // holds a copy of the key: layers may change mid-press PointerTracker tracker; // has its own copy of the key: the layer may change mid-press
int index = -1; // pressed key in the model, -1 once the layer changed int index = -1; // pressed key in the model, -1 once the layer changed
int hoverIndex = -1; // key under the finger while sliding int hoverIndex = -1; // key under the finger while sliding
quint64 serial = 0; // tells a reused touch id from the original quint64 serial = 0; // distinguishes a reused touch id from the original
}; };
void pointerDown(int id, QPointF pos, qint64 timeMs); void pointerDown(int id, QPointF pos, qint64 timeMs);
@@ -82,7 +81,7 @@ private:
quint64 m_alternatesSerial = 0; // the pointer the popup belongs to quint64 m_alternatesSerial = 0; // the pointer the popup belongs to
QHash<int, Pointer> m_pointers; QHash<int, Pointer> m_pointers;
quint64 m_nextSerial = 1; quint64 m_nextSerial = 1;
qint64 m_lastEventMs = 0; // timers have no event time; they use this qint64 m_lastEventMs = 0; // timestamp of the last event; used as "now" in timer callbacks
bool m_trackpadActive = false; bool m_trackpadActive = false;
qreal m_popupHeadroom = std::numeric_limits<qreal>::max(); qreal m_popupHeadroom = std::numeric_limits<qreal>::max();
}; };
+4 -4
View File
@@ -9,19 +9,19 @@
#include <array> #include <array>
// Ctrl, Alt, Super and AltGr, each sticky like shift (see ShiftState): tap // Ctrl, Alt, Super and AltGr, each sticky like shift (see ShiftState): tap
// for the next key, double-tap to lock, hold while typing with another finger. // for the next key, double-tap to lock, or hold while typing with another finger.
// The first three turn keys into shortcuts; AltGr picks their AltGr symbols. // Ctrl/Alt/Super make keys send shortcuts; AltGr selects their AltGr symbols.
class Modifiers class Modifiers
{ {
public: public:
// Names as used in layouts: "ctrl", "alt", "super", "altgr". // Names as used in layouts: "ctrl", "alt", "super", "altgr".
void press(const QString &name, qint64 timeMs); void press(const QString &name, qint64 timeMs);
void release(const QString &name); void release(const QString &name);
// A key was typed: one-shot modifiers are used up. // Call after a key is typed: clears one-shot modifiers.
void keyTyped(); void keyTyped();
void clear(); void clear();
// Ctrl, Alt and Super: what makes a key a shortcut. // Active Ctrl/Alt/Super, i.e. the ones that make a key a shortcut.
Qt::KeyboardModifiers active() const; Qt::KeyboardModifiers active() const;
bool isAltGrActive() const; bool isAltGrActive() const;
// name → ShiftState::Mode as int, for the view. // name → ShiftState::Mode as int, for the view.
+2 -2
View File
@@ -29,7 +29,7 @@ void PointerTracker::moveTo(QPointF pos)
if (m_key.type == KeyType::LayerSwitch) { if (m_key.type == KeyType::LayerSwitch) {
m_gesture = Gesture::Slide; m_gesture = Gesture::Slide;
} else if (m_config.gestures && m_key.type == KeyType::Space && qAbs(delta.x()) > qAbs(delta.y())) { } else if (m_config.gestures && m_key.type == KeyType::Space && qAbs(delta.x()) > qAbs(delta.y())) {
// From the press point: the slop counts towards the first step. // Measured from the press point, so the slop distance counts towards the first step.
m_gesture = Gesture::Trackpad; m_gesture = Gesture::Trackpad;
m_trackpadOrigin = m_start; m_trackpadOrigin = m_start;
m_trackpadLines = false; m_trackpadLines = false;
@@ -68,7 +68,7 @@ QPoint PointerTracker::takeTrackpadSteps()
return {}; return {};
const QPointF travel = m_position - m_trackpadOrigin; const QPointF travel = m_position - m_trackpadOrigin;
// Truncate towards zero so jitter around the origin doesn't step. // Truncate towards zero so jitter around the origin doesn't produce steps.
const int rows = m_trackpadLines ? int(travel.y() / m_config.trackpadRowStep) : 0; const int rows = m_trackpadLines ? int(travel.y() / m_config.trackpadRowStep) : 0;
const QPoint total(int(travel.x() / m_config.trackpadStep), rows); const QPoint total(int(travel.x() / m_config.trackpadStep), rows);
const QPoint steps = total - m_trackpadStepsTaken; const QPoint steps = total - m_trackpadStepsTaken;
+1 -1
View File
@@ -8,7 +8,7 @@
// Gesture recognition for one finger on one key. Pure logic: the caller feeds // Gesture recognition for one finger on one key. Pure logic: the caller feeds
// positions and tells it when the long-press delay has elapsed. // positions and tells it when the long-press delay has elapsed.
// //
// Tap pressed, not (yet) anything else // Tap pressed, no other gesture recognized (yet)
// Flick moved down on a key with a flick symbol // Flick moved down on a key with a flick symbol
// LongPress held still on a key with alternates // LongPress held still on a key with alternates
// Trackpad held still on space; movement moves the text cursor. Sliding // Trackpad held still on space; movement moves the text cursor. Sliding
+3 -3
View File
@@ -2,8 +2,8 @@
void ShiftState::press(qint64 timeMs) void ShiftState::press(qint64 timeMs)
{ {
// Locks whatever the first tap did: at a sentence start that tap turns // A double tap always locks, even when the first tap turned auto-shift off
// auto-shift off, and the second must still lock. // (at a sentence start).
const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs && !m_lastPressUnlocked; const bool doubleTap = timeMs - m_lastPress <= DoubleTapMs && !m_lastPressUnlocked;
m_lastPress = timeMs; m_lastPress = timeMs;
m_lastPressUnlocked = m_mode == Mode::Locked; m_lastPressUnlocked = m_mode == Mode::Locked;
@@ -28,7 +28,7 @@ void ShiftState::release()
void ShiftState::characterTyped() void ShiftState::characterTyped()
{ {
m_lastPress = NoPress; // shift, type, shift is not a double tap m_lastPress = NoPress; // shift, type, shift must not count as a double tap
m_userChoice = false; m_userChoice = false;
if (m_held) if (m_held)
m_typedWhileHeld = true; m_typedWhileHeld = true;
+7 -8
View File
@@ -18,20 +18,19 @@ public:
Mode mode() const { return m_mode; } Mode mode() const { return m_mode; }
bool isActive() const { return m_mode != Mode::Off; } bool isActive() const { return m_mode != Mode::Off; }
// Whether shift joins shortcuts and non-text keys (Shift+F5): only when // Whether shift is added to shortcuts and non-text keys (Shift+F5): only
// tapped or held by the user. Neither auto-capitalization nor caps lock // if the user tapped or held it, not for auto-capitalization or caps lock (as on a PC).
// does, as on a PC.
bool appliesToShortcuts() const { return m_held || (m_mode == Mode::Once && !m_automatic); } bool appliesToShortcuts() const { return m_held || (m_mode == Mode::Once && !m_automatic); }
void press(qint64 timeMs); void press(qint64 timeMs);
void release(); void release();
void characterTyped(); void characterTyped();
// Auto-capitalization hint; ignored while shift is held or locked. A shift // Auto-capitalization hint; ignored while shift is held or locked. A
// the user tapped stays until a character is typed or the hint changes, // user-tapped shift is kept until a character is typed or the hint value
// so a field re-reporting the same context doesn't undo it. // changes, so the field re-sending the same context doesn't reset it.
void setAutomatic(bool capitalize); void setAutomatic(bool capitalize);
// A new field: the user's shift choice no longer applies. // New field focused: discard the user's manual shift.
void forgetUserChoice() { m_userChoice = false; } void forgetUserChoice() { m_userChoice = false; }
// Caps lock key: locks, or unlocks if locked. // Caps lock key: locks, or unlocks if locked.
void toggleLock(); void toggleLock();
@@ -43,7 +42,7 @@ private:
bool m_userChoice = false; // the user set the mode since the last character bool m_userChoice = false; // the user set the mode since the last character
bool m_held = false; bool m_held = false;
bool m_typedWhileHeld = false; bool m_typedWhileHeld = false;
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap bool m_lastPressUnlocked = false; // the tap that unlocks caps lock must not start a double tap
static constexpr qint64 NoPress = std::numeric_limits<qint64>::min() / 2; static constexpr qint64 NoPress = std::numeric_limits<qint64>::min() / 2;
qint64 m_lastPress = NoPress; qint64 m_lastPress = NoPress;
}; };
+1 -1
View File
@@ -2,7 +2,7 @@
#include <QString> #include <QString>
// Pure text heuristics behind the iOS conveniences. // Text heuristics for the iOS-style typing aids. No state.
namespace TextRules { namespace TextRules {
// True at the start of a sentence: empty text, after a newline, or after // True at the start of a sentence: empty text, after a newline, or after
+3 -3
View File
@@ -13,7 +13,7 @@ qreal blockUnits(const Block &block)
return units; return units;
} }
// Key widths in units once stretch keys take up the row's slack. // Key widths in units, after stretch keys are widened to fill the row.
QList<qreal> stretchedWidths(const Row &row, qreal targetUnits) QList<qreal> stretchedWidths(const Row &row, qreal targetUnits)
{ {
const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch); const auto stretchy = std::ranges::count_if(row, &KeyDef::stretch);
@@ -44,7 +44,7 @@ qreal totalUnits(const QList<Block> &blocks)
qsizetype totalRows(const QList<Block> &blocks) qsizetype totalRows(const QList<Block> &blocks)
{ {
// Filling blocks only count when nothing else is shown. // fillHeight blocks are ignored, unless all blocks are fillHeight.
const bool allFill = std::ranges::all_of(blocks, &Block::fillHeight); const bool allFill = std::ranges::all_of(blocks, &Block::fillHeight);
qsizetype rows = 0; qsizetype rows = 0;
for (const Block &block : blocks) { for (const Block &block : blocks) {
@@ -78,7 +78,7 @@ QList<QRectF> computeKeyRects(const QList<Block> &blocks, QSizeF size, KeyboardM
const auto rows = block.rows.size(); const auto rows = block.rows.size();
const qreal span = block.fillHeight ? innerHeight : spanOf(std::max(rows, block.minRows)); const qreal span = block.fillHeight ? innerHeight : spanOf(std::max(rows, block.minRows));
const qsizetype fixed = std::min(block.fixedRows, rows); const qsizetype fixed = std::min(block.fixedRows, rows);
// Rows after the fixed ones share what's left; with none, the block sits at the bottom. // Rows after fixedRows split the remaining height; if there are none, the block is bottom-aligned.
const qreal flexHeight = rows > fixed const qreal flexHeight = rows > fixed
? (span - qreal(fixed) * (sharedRowHeight + metrics.gap) - qreal(rows - fixed - 1) * metrics.gap) ? (span - qreal(fixed) * (sharedRowHeight + metrics.gap) - qreal(rows - fixed - 1) * metrics.gap)
/ qreal(rows - fixed) / qreal(rows - fixed)
+2 -2
View File
@@ -12,8 +12,8 @@ struct KeyboardMetrics {
}; };
// The keys on screen: blocks of rows, side by side. // The keys on screen: blocks of rows, side by side.
// A block can reserve room (minUnits, minRows) so that swapping its rows, e.g. // A block can reserve space (minUnits, minRows), so swapping its rows (e.g.
// switching layers, moves nothing else; its keys stretch into the room. // switching layers) doesn't move other blocks. Its keys stretch to fill it.
struct Block { struct Block {
QList<Row> rows; QList<Row> rows;
bool fillHeight = false; // spans the full height instead of max(rows, minRows) shared rows bool fillHeight = false; // spans the full height instead of max(rows, minRows) shared rows
+1 -1
View File
@@ -34,5 +34,5 @@ struct KeymapKeys {
KeymapKeys readKeymapKeys(xkb_keymap *keymap, quint32 layoutIndex); KeymapKeys readKeymapKeys(xkb_keymap *keymap, quint32 layoutIndex);
// What a dead key shows and types when nothing composes: ´ for dead_acute. // Spacing accent for a dead keysym, used as its label and when nothing composes: ´ for dead_acute.
QString deadKeyText(quint32 keysym); QString deadKeyText(quint32 keysym);
+12 -12
View File
@@ -19,7 +19,7 @@ const QHash<QString, QString> &baseOverrides()
return bases; return bases;
} }
// "š" → "s": the letter an accented letter is a variant of. // Base letter of an accented letter: "š" → "s".
QString baseLetter(const QString &letter) QString baseLetter(const QString &letter)
{ {
if (const auto it = baseOverrides().constFind(letter); it != baseOverrides().cend()) if (const auto it = baseOverrides().constFind(letter); it != baseOverrides().cend())
@@ -41,7 +41,7 @@ KeyDef characterKey(const KeymapKey &source)
return key; return key;
} }
// The compact letter rows: letters only, and not the keys some keyboards lack. // Compact-mode letter rows: letters only, without keys missing on some keyboards (BKSL, LSGT).
QList<KeyDef> letterKeys(const KeymapKeys &keymap, int row, const QStringList &flicks) QList<KeyDef> letterKeys(const KeymapKeys &keymap, int row, const QStringList &flicks)
{ {
QList<KeyDef> keys; QList<KeyDef> keys;
@@ -49,7 +49,7 @@ QList<KeyDef> letterKeys(const KeymapKeys &keymap, int row, const QStringList &f
if (!source.isLetter() || source.position != KeymapKey::Position::Regular) if (!source.isLetter() || source.position != KeymapKey::Position::Regular)
continue; continue;
KeyDef key = characterKey(source); KeyDef key = characterKey(source);
// iPad letters always uppercase; the keymap may shift some to a symbol (sk: ô → "). // Shifted letters are always uppercased (as on iPad), even where the keymap's shift level is a symbol (sk: ô → ").
key.shifted = source.lower.toUpper(); key.shifted = source.lower.toUpper();
key.deadShifted = 0; key.deadShifted = 0;
key.flick = flicks.value(keys.size()); key.flick = flicks.value(keys.size());
@@ -58,13 +58,13 @@ QList<KeyDef> letterKeys(const KeymapKeys &keymap, int row, const QStringList &f
return keys; return keys;
} }
// A full row: every typing key, the shifted symbol shown (and flickable) above; // Full-mode row: every typing key. The secondary (flick) symbol is the shifted
// for letters, whose shift only capitalizes, the AltGr symbol (sk: ' on j). // symbol, or for letters (where shift only capitalizes) the AltGr symbol (sk: ' on j).
// `withLessGreater` tells whether the ISO key left of Z is among them. // `withLessGreater` is set if the row contains the ISO key left of Z.
QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater) QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
{ {
*withLessGreater = false; *withLessGreater = false;
QSet<QString> elsewhere; // LSGT often repeats a pair that exists elsewhere (US: < >) QSet<QString> elsewhere; // LSGT often duplicates symbols on other keys (US: < >)
for (const auto &keys : keymap.rows) { for (const auto &keys : keymap.rows) {
for (const KeymapKey &key : keys) { for (const KeymapKey &key : keys) {
if (key.position != KeymapKey::Position::LessGreater) if (key.position != KeymapKey::Position::LessGreater)
@@ -78,7 +78,7 @@ QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
continue; continue;
*withLessGreater |= source.position == KeymapKey::Position::LessGreater; *withLessGreater |= source.position == KeymapKey::Position::LessGreater;
KeyDef key = characterKey(source); KeyDef key = characterKey(source);
// Show (and flick for) a shifted level that isn't just the capital letter: ! on 1, " on sk ô. // Use the shifted level as the flick symbol unless it's just the capital letter: ! on 1, " on sk ô.
if (!source.deadUpper && source.upper != source.lower && source.upper != source.lower.toUpper()) if (!source.deadUpper && source.upper != source.lower && source.upper != source.lower.toUpper())
key.flick = source.upper; key.flick = source.upper;
else if (!source.deadAltGr && source.altGr != source.lower) else if (!source.deadAltGr && source.altGr != source.lower)
@@ -97,7 +97,7 @@ Row expandPlaceholders(const Row &row, const KeymapKeys &keymap)
else if (key.type == KeyType::Keys) { else if (key.type == KeyType::Keys) {
bool withLessGreater = false; bool withLessGreater = false;
const QList<KeyDef> keys = rowKeys(keymap, key.lettersRow, &withLessGreater); const QList<KeyDef> keys = rowKeys(keymap, key.lettersRow, &withLessGreater);
// As on ISO keyboards, that key takes its room from the one before it (left shift). // As on ISO keyboards, LSGT's width is taken from the key before it (left shift).
if (withLessGreater && !result.isEmpty()) if (withLessGreater && !result.isEmpty())
result.back().width = std::max(1.0, result.back().width - 1); result.back().width = std::max(1.0, result.back().width - 1);
result += keys; result += keys;
@@ -108,9 +108,9 @@ Row expandPlaceholders(const Row &row, const KeymapKeys &keymap)
return result; return result;
} }
// Long-press choices per letter: the keymap's letters that have no letter key // Long-press choices per letter: first the keymap's letters that have no key of
// first (they're what this language types), then the template's. Letters on the // their own in the letter rows (the language's own letters), then the template's.
// number row (sk: ť) stay choices in full mode too: they're out of the way there. // Number-row letters (sk: ť) are kept as choices in full mode too, since the number row is hard to reach.
void addAlternates(Layer &layer, const Layout &tmpl, const KeymapKeys &keymap) void addAlternates(Layer &layer, const Layout &tmpl, const KeymapKeys &keymap)
{ {
QSet<QString> onKeys; QSet<QString> onKeys;
+3 -3
View File
@@ -8,7 +8,7 @@ enum class KeyboardMode {
Full, // iPad Pro / PC: number row, punctuation, tab, modifiers Full, // iPad Pro / PC: number row, punctuation, tab, modifiers
}; };
// Turns the template into the layout shown: picks each layer's rows for the // Builds the displayed layout from the template: picks each layer's rows for
// mode, fills the keymap placeholders, and gives letters their long-press // the mode, fills the keymap placeholders, and assigns long-press choices to
// choices (letters of the keymap without a key of their own come first). // letters (keymap letters without their own key first).
Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode = KeyboardMode::Compact); Layout buildLayout(const Layout &tmpl, const KeymapKeys &keymap, KeyboardMode mode = KeyboardMode::Compact);
+3 -3
View File
@@ -49,7 +49,7 @@ QVariant LayoutModel::data(const QModelIndex &index, int role) const
const QRectF rect = m_rects.value(index.row()); const QRectF rect = m_rects.value(index.row());
switch (role) { switch (role) {
case LabelRole: return key.text(m_keyLevel); case LabelRole: return key.text(m_keyLevel);
// A shifted or AltGr key may already show its flick symbol (! on 1): don't repeat it. // Hide the flick symbol if the main label already shows it (shifted or AltGr: ! on 1).
case FlickLabelRole: return key.flick == key.text(m_keyLevel) ? QString() : key.flick; case FlickLabelRole: return key.flick == key.text(m_keyLevel) ? QString() : key.flick;
case ModifierRole: return key.modifier; case ModifierRole: return key.modifier;
case KeyTypeRole: return keyTypeName(key.type); case KeyTypeRole: return keyTypeName(key.type);
@@ -185,7 +185,7 @@ QList<Block> LayoutModel::composeBlocks() const
main.rows += section.rows; main.rows += section.rows;
} }
if (layer && shown(MainSectionId)) { if (layer && shown(MainSectionId)) {
// Room for the biggest layer, so switching layers keeps the other blocks still. // Reserve space for the largest layer, so switching layers doesn't move other blocks.
main.fixedRows = main.rows.size(); main.fixedRows = main.rows.size();
for (const Layer &other : m_layout.layers) { for (const Layer &other : m_layout.layers) {
main.minRows = std::max(main.minRows, main.fixedRows + other.rows.size()); main.minRows = std::max(main.minRows, main.fixedRows + other.rows.size());
@@ -202,7 +202,7 @@ QList<Block> LayoutModel::composeBlocks() const
if (section.placement == Section::Placement::Right && shown(section.id)) if (section.placement == Section::Placement::Right && shown(section.id))
blocks.append({section.rows, section.fillHeight}); blocks.append({section.rows, section.fillHeight});
} }
// Never show nothing. // If no block is shown, fall back to the current layer.
if (blocks.isEmpty() && layer) if (blocks.isEmpty() && layer)
blocks.append({layer->rows}); blocks.append({layer->rows});
return blocks; return blocks;
+12 -12
View File
@@ -46,8 +46,8 @@ std::optional<Layout> loadTemplate()
return std::nullopt; return std::nullopt;
} }
// The shown layout is the template filled with the active system layout's // Builds the displayed layout from the template and the active system layout.
// keys. Returns the rebuild, for settings that change the result. // Returns the rebuild function, for settings that affect the result.
std::function<void()> connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts, std::function<void()> connectLayouts(const Layout &tmpl, WaylandKeymap &keymap, SystemLayouts &systemLayouts,
const Settings &settings, KeyboardController &controller, LayoutModel &model) const Settings &settings, KeyboardController &controller, LayoutModel &model)
{ {
@@ -63,7 +63,7 @@ std::function<void()> connectLayouts(const Layout &tmpl, WaylandKeymap &keymap,
return rebuild; return rebuild;
} }
// Settings that add or remove keys resize the window so keys keep their size. // Settings that add or remove keys resize the window, so key size stays the same.
void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &placement, const QQuickWindow &window, void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &placement, const QQuickWindow &window,
const std::function<void()> &rebuild) const std::function<void()> &rebuild)
{ {
@@ -83,7 +83,7 @@ void connectKeySettings(Settings &settings, LayoutModel &model, FloatingWindow &
[keepingKeySize, rebuild] { keepingKeySize(rebuild); }); [keepingKeySize, rebuild] { keepingKeySize(rebuild); });
} }
// The shrunk button's geometry isn't the keyboard's: not saved. // Geometry isn't saved while collapsed (it's the button's, not the keyboard's).
void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings, const CollapseController &collapse) void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settings, const CollapseController &collapse)
{ {
if (const auto geometry = settings.load(placement.screenName())) if (const auto geometry = settings.load(placement.screenName()))
@@ -94,8 +94,8 @@ void restoreAndPersistGeometry(FloatingWindow &placement, WindowSettings &settin
}); });
} }
// A focused field shows the keyboard only if KWin's policy wants one now; // On field focus, ask KWin's policy first and show the keyboard only if it
// asked before showing, so a "no" doesn't flash the keyboard. // agrees. Asking before showing avoids a brief flash when the answer is no.
void connectFieldFocus(VisibilityController &visibility, KWinInputMethod &backend, KWinVirtualKeyboard &policy) void connectFieldFocus(VisibilityController &visibility, KWinInputMethod &backend, KWinVirtualKeyboard &policy)
{ {
QObject::connect(&backend, &TextBackend::activeChanged, &visibility, [&] { QObject::connect(&backend, &TextBackend::activeChanged, &visibility, [&] {
@@ -105,7 +105,7 @@ void connectFieldFocus(VisibilityController &visibility, KWinInputMethod &backen
} }
policy.askShowOnActive(&visibility, [&](bool show) { policy.askShowOnActive(&visibility, [&](bool show) {
if (!backend.isActive()) if (!backend.isActive())
return; // left the field before KWin answered return; // field lost focus before KWin answered
visibility.setShowOnField(show); visibility.setShowOnField(show);
visibility.setFieldActive(true); visibility.setFieldActive(true);
}); });
@@ -118,14 +118,14 @@ void connectFieldFocus(VisibilityController &visibility, KWinInputMethod &backen
void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, const Settings &settings, void connectVisibility(VisibilityController &visibility, KWinInputMethod &backend, const Settings &settings,
FloatingWindow &placement) FloatingWindow &placement)
{ {
// First, so the initial state below reaches the window too. // Connect first, so the initial state set below is applied to the window.
QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement, QObject::connect(&visibility, &VisibilityController::visibleChanged, &placement,
[&](bool visible) { visible ? placement.show() : placement.hide(); }); [&](bool visible) { visible ? placement.show() : placement.hide(); });
visibility.setAlwaysVisible(settings.alwaysVisible()); visibility.setAlwaysVisible(settings.alwaysVisible());
QObject::connect(&settings, &Settings::alwaysVisibleChanged, &visibility, QObject::connect(&settings, &Settings::alwaysVisibleChanged, &visibility,
[&] { visibility.setAlwaysVisible(settings.alwaysVisible()); }); [&] { visibility.setAlwaysVisible(settings.alwaysVisible()); });
// Without the input-method global (run outside KWin's control, e.g. for UI // Without the input-method global (not launched by KWin, e.g. during UI
// work) there is no focus signal: stay visible so it can be tried out. // development) there are no focus events: stay visible.
const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); }; const auto updateStandalone = [&] { visibility.setStandalone(!backend.isAvailable()); };
QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone); QObject::connect(&backend, &KWinInputMethod::availableChanged, &visibility, updateStandalone);
updateStandalone(); updateStandalone();
@@ -154,10 +154,10 @@ void connectTextContext(KWinInputMethod &backend, KeyboardController &controller
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
blockQuitSignals(); blockQuitSignals();
// libwayland unsets it once connected. // Read before connecting: libwayland unsets it.
const bool launchedByKWin = qEnvironmentVariableIsSet("WAYLAND_SOCKET"); const bool launchedByKWin = qEnvironmentVariableIsSet("WAYLAND_SOCKET");
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
// KWin relaunches it only after a crash: a closed window mustn't quit it. // Don't quit when the window closes: KWin only restarts the input method after a crash.
QGuiApplication::setQuitOnLastWindowClosed(false); QGuiApplication::setQuitOnLastWindowClosed(false);
quitOnSignal(); quitOnSignal();
QGuiApplication::setApplicationName(QStringLiteral("sloposk")); QGuiApplication::setApplicationName(QStringLiteral("sloposk"));
+2 -2
View File
@@ -4,8 +4,8 @@
#include <QTimer> #include <QTimer>
#include <QVariantMap> #include <QVariantMap>
// QSettings writes that arrive in bursts (dragging a slider or the window), // Debounced QSettings writes: values that change rapidly (slider or window
// saved once the burst ends instead of on every step. Flushes on destruction. // drag) are saved once changes stop. Flushes on destruction.
class DebouncedStore : public QObject class DebouncedStore : public QObject
{ {
Q_OBJECT Q_OBJECT
+1 -1
View File
@@ -44,7 +44,7 @@ public:
void setHoldKeys(bool enabled); void setHoldKeys(bool enabled);
QStringList visibleSections() const { return m_visibleSections; } QStringList visibleSections() const { return m_visibleSections; }
// Hiding the last visible section is refused: something must stay on screen. // Refuses to hide the last visible section.
Q_INVOKABLE void setSectionVisible(const QString &id, bool visible); Q_INVOKABLE void setSectionVisible(const QString &id, bool visible);
bool fullKeyboard() const { return m_fullKeyboard; } bool fullKeyboard() const { return m_fullKeyboard; }
+4 -4
View File
@@ -37,7 +37,7 @@ bool CollapseController::collapse()
m_expandedSize = m_window.size(); m_expandedSize = m_window.size();
m_collapsed = true; m_collapsed = true;
Q_EMIT collapsedChanged(); Q_EMIT collapsedChanged();
// After the next frame, once the compositor knows the lower minimum size. // Wait one frame, so the compositor has the new (lower) minimum size before the resize.
connect(&m_window, &QQuickWindow::frameSwapped, this, [this] { connect(&m_window, &QQuickWindow::frameSwapped, this, [this] {
if (m_collapsed) if (m_collapsed)
m_placement.moveResize(m_keyRect, false); m_placement.moveResize(m_keyRect, false);
@@ -50,10 +50,10 @@ void CollapseController::expand()
if (!m_collapsed || m_expanding.isActive()) if (!m_collapsed || m_expanding.isActive())
return; return;
if (roughlyEqual(m_window.size(), m_expandedSize)) { if (roughlyEqual(m_window.size(), m_expandedSize)) {
finishExpanding(); // never shrank: nothing to move finishExpanding(); // window never shrank: nothing to resize
return; return;
} }
// Stays collapsed until the window has grown, so the keys don't flash squeezed. // Stay collapsed until the window has grown, so the keys aren't drawn squeezed.
m_placement.moveResize(QRectF(-m_keyRect.topLeft(), m_expandedSize), true); m_placement.moveResize(QRectF(-m_keyRect.topLeft(), m_expandedSize), true);
m_expanding.start(); m_expanding.start();
} }
@@ -68,7 +68,7 @@ void CollapseController::finishExpanding()
{ {
m_expanding.stop(); m_expanding.stop();
if (!roughlyEqual(m_window.size(), m_expandedSize)) if (!roughlyEqual(m_window.size(), m_expandedSize))
m_window.resize(m_expandedSize); // no KWin to do it m_window.resize(m_expandedSize); // KWin didn't resize (timeout): resize directly
m_collapsed = false; m_collapsed = false;
Q_EMIT collapsedChanged(); Q_EMIT collapsedChanged();
} }
+7 -6
View File
@@ -8,15 +8,16 @@
class FloatingWindow; class FloatingWindow;
class QQuickWindow; class QQuickWindow;
// Shrinks the keyboard window to just its dismiss key, floating on its own, // Collapses the keyboard window to the size of its dismiss key (a floating
// and grows it back with the key under the button again (kept on screen). // button) and expands it back, positioned so the dismiss key is where the
// The window itself shrinks, so it can be dragged anywhere and covers // button was (clamped to the screen). The window itself is resized, so the
// nothing else. QML draws from `collapsed` and `keyRect`. // button can be dragged anywhere and covers nothing else.
// QML uses `collapsed` and `keyRect`.
class CollapseController : public QObject class CollapseController : public QObject
{ {
Q_OBJECT Q_OBJECT
Q_PROPERTY(bool collapsed READ isCollapsed NOTIFY collapsedChanged) Q_PROPERTY(bool collapsed READ isCollapsed NOTIFY collapsedChanged)
// The key in the full window; the button stays there until the window shrinks. // Dismiss key rect in the full window. The button is drawn there until the window has shrunk.
Q_PROPERTY(QRectF keyRect READ keyRect NOTIFY collapsedChanged) Q_PROPERTY(QRectF keyRect READ keyRect NOTIFY collapsedChanged)
public: public:
@@ -41,5 +42,5 @@ private:
bool m_collapsed = false; bool m_collapsed = false;
QRectF m_keyRect; QRectF m_keyRect;
QSize m_expandedSize; QSize m_expandedSize;
QTimer m_expanding; // waits for KWin; runs out without it QTimer m_expanding; // timeout for KWin's resize; fires if KWin never resizes
}; };
+3 -3
View File
@@ -22,7 +22,7 @@ FloatingWindow::FloatingWindow(QWindow *window, QObject *parent)
bool FloatingWindow::eventFilter(QObject *watched, QEvent *event) bool FloatingWindow::eventFilter(QObject *watched, QEvent *event)
{ {
// Exposed means mapped, so KWin knows the window by now. // Exposed implies mapped, so KWin knows the window at this point.
if (watched == m_window && event->type() == QEvent::Expose) { if (watched == m_window && event->type() == QEvent::Expose) {
if (m_window->isExposed() && !m_exposed) if (m_window->isExposed() && !m_exposed)
moveResize(QRectF(QPointF(), m_window->size()), true); moveResize(QRectF(QPointF(), m_window->size()), true);
@@ -107,8 +107,8 @@ void FloatingWindow::startResize(Qt::Edges edges)
releaseMouse(); releaseMouse();
} }
// The window manager keeps the button's release: without one, the pressed // After a system move/resize the window never gets the mouse release. Send
// item holds on to the mouse and eats the next press. // one, otherwise the pressed item keeps the mouse grab and swallows the next press.
void FloatingWindow::releaseMouse() void FloatingWindow::releaseMouse()
{ {
if (!(QGuiApplication::mouseButtons() & Qt::LeftButton)) if (!(QGuiApplication::mouseButtons() & Qt::LeftButton))
+9 -9
View File
@@ -6,12 +6,12 @@
class QWindow; class QWindow;
// Makes a keyboard window a frameless, always-on-top window that never takes // Makes the keyboard window frameless, always on top and never focused.
// focus. The window manager places it and moves it (Super+drag included); // The window manager places and moves it (including Super+drag); on KWin
// on KWin Wayland that relies on the rule from installKWinRule(). // Wayland this needs the rule from installKWinRule().
// Each time it appears, it's moved back on screen: KWin remembers where it // On each show, the window is moved back onto the screen: KWin restores the
// was, which may be somewhere it was smaller. Geometry is relative to the // last position, which may be off-screen at the current size.
// window's screen. // Geometry is relative to the window's screen.
class FloatingWindow : public QObject class FloatingWindow : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -27,9 +27,9 @@ public:
// Grows from the top-left corner; respects the window's minimum size. // Grows from the top-left corner; respects the window's minimum size.
Q_INVOKABLE void resizeBy(qreal dw, qreal dh); Q_INVOKABLE void resizeBy(qreal dw, qreal dh);
// Moves the window by `relative`'s offset and resizes it to its size, // Moves the window by `relative`'s offset and resizes it to `relative`'s
// optionally clamped to the screen's free area. Done by KWin (Wayland // size, optionally clamped to the screen's available area. Done by KWin
// clients can't place themselves), so it applies later. // (Wayland clients can't position themselves), so it's asynchronous.
void moveResize(const QRectF &relative, bool keepOnScreen); void moveResize(const QRectF &relative, bool keepOnScreen);
// Only this part of the window takes input; the rest passes through. // Only this part of the window takes input; the rest passes through.
Q_INVOKABLE void setInputRegion(const QRectF &region); Q_INVOKABLE void setInputRegion(const QRectF &region);
+1 -1
View File
@@ -31,7 +31,7 @@ void runKWinScript(const QString &source)
{ {
static int count = 0; static int count = 0;
const QString plugin = QStringLiteral("sloposk-%1").arg(++count); const QString plugin = QStringLiteral("sloposk-%1").arg(++count);
// Unloading from within: `stop` right after `run` could beat the script. // The script unloads itself: calling `stop` right after `run` could unload it before it runs.
const QString unload = QStringLiteral("\ncallDBus(\"%1\", \"%2\", \"%3\", \"unloadScript\", \"%4\");\n") const QString unload = QStringLiteral("\ncallDBus(\"%1\", \"%2\", \"%3\", \"unloadScript\", \"%4\");\n")
.arg(Service, ScriptingPath, ScriptingInterface, plugin); .arg(Service, ScriptingPath, ScriptingInterface, plugin);
+10 -10
View File
@@ -3,13 +3,12 @@
#include <QObject> #include <QObject>
#include <optional> #include <optional>
// Decides whether the keyboard is shown: // Decides whether the keyboard is visible:
// - while a text field is focused, if the system wants a keyboard for it // - hidden while the system virtual keyboard toggle is off (overrides everything)
// - always, if the user wants it (to type into any window) or when running // - show()/hide()/toggle() override the rules below until the next focus change
// standalone (no input-method connection) // (including focus moving directly from one field to another)
// - show()/hide()/toggle() override that until the focus changes, also // - shown if "always visible" is set, or when standalone (no input-method connection)
// from one field straight to another // - shown while a text field is focused, if KWin's policy allows it
// - never while the system's virtual keyboard toggle is off
class VisibilityController : public QObject class VisibilityController : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -19,13 +18,14 @@ public:
bool isVisible() const { return m_visible; } bool isVisible() const { return m_visible; }
// Called on every focus change, even when a field follows a field. // Call on every focus change, including field-to-field.
void setFieldActive(bool active); void setFieldActive(bool active);
// KWin's policy (off, or touch only) for fields focused from now on. // Whether KWin's policy (disabled / touch only) allows showing for the focused field.
void setShowOnField(bool show); void setShowOnField(bool show);
void setStandalone(bool standalone); void setStandalone(bool standalone);
void setAlwaysVisible(bool always); void setAlwaysVisible(bool always);
// The system toggle (KWin's tray icon); off hides it whatever else says. // System virtual keyboard toggle (KWin tray icon). When false, the keyboard
// is always hidden, regardless of the other settings. Clears show()/hide() overrides.
void setEnabled(bool enabled); void setEnabled(bool enabled);
void show(); void show();
+2 -2
View File
@@ -6,8 +6,8 @@
#include <QRect> #include <QRect>
#include <optional> #include <optional>
// Remembers the keyboard's geometry per screen (~/.config/sloposk.conf). // Stores the keyboard's geometry per screen (~/.config/sloposk/sloposk.conf).
// Saving is debounced so dragging doesn't hammer the disk. // Saving is debounced, so dragging doesn't write on every step.
class WindowSettings : public QObject class WindowSettings : public QObject
{ {
Q_OBJECT Q_OBJECT
+1 -1
View File
@@ -443,7 +443,7 @@ private Q_SLOTS:
QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), 1); QCOMPARE(controller->modifierModes().value(u"ctrl"_s).toInt(), 1);
controller->setOptions({}); // turned off while held controller->setOptions({}); // turned off while held
QVERIFY(engine.held.isEmpty()); QVERIFY(engine.held.isEmpty());
controller->keyUp(ctrl, KeyOutput::Primary, {}, 10); // back to sticky rules: harmless controller->keyUp(ctrl, KeyOutput::Primary, {}, 10); // released under sticky rules: no effect
QVERIFY(engine.held.isEmpty()); QVERIFY(engine.held.isEmpty());
} }
+2 -2
View File
@@ -131,11 +131,11 @@ private Q_SLOTS:
QTest::touchEvent(&window, touchscreen).press(0, e); QTest::touchEvent(&window, touchscreen).press(0, e);
QTest::qWait(LongPressWaitMs); QTest::qWait(LongPressWaitMs);
QTest::touchEvent(&window, touchscreen).stationary(0).press(1, u); QTest::touchEvent(&window, touchscreen).stationary(0).press(1, u);
QTest::qWait(LongPressWaitMs); // u's popup replaces e's, which types its choice QTest::qWait(LongPressWaitMs); // u's popup opens; e's popup closes and types its selection
QCOMPARE(engine.output, u"é"); QCOMPARE(engine.output, u"é");
QVERIFY(surface->alternates()->isOpen()); QVERIFY(surface->alternates()->isOpen());
QTest::touchEvent(&window, touchscreen).release(0, e).stationary(1); QTest::touchEvent(&window, touchscreen).release(0, e).stationary(1);
QCOMPARE(engine.output, u"é"); // the first finger's lift does nothing more QCOMPARE(engine.output, u"é"); // lifting the first finger types nothing more
QVERIFY(surface->alternates()->isOpen()); QVERIFY(surface->alternates()->isOpen());
QTest::touchEvent(&window, touchscreen).release(1, u); QTest::touchEvent(&window, touchscreen).release(1, u);
QCOMPARE(engine.output.size(), 2); QCOMPARE(engine.output.size(), 2);
+1 -1
View File
@@ -79,7 +79,7 @@ private Q_SLOTS:
v.show(); v.show();
QVERIFY(!v.isVisible()); QVERIFY(!v.isVisible());
v.hide(); v.hide();
v.setEnabled(true); // turning it back on forgets the dismiss v.setEnabled(true); // re-enabling clears the earlier hide()
QVERIFY(v.isVisible()); QVERIFY(v.isVisible());
} }