Add an AltGr key; full-mode letters flick to their AltGr symbol
The right alt in full mode is now AltGr: sticky like the other modifiers, it types the keymap's third/fourth level (sk: AltGr+J = ') instead of an Alt shortcut. In hold-keys mode it holds the physical right alt, since KWin resets the modifier state after an ISO_Level3_Shift keysym. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,9 @@ iPad-style floating on-screen keyboard for Plasma Wayland. Qt 6, C++ core, QML U
|
|||||||
- 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
|
||||||
- Slide sideways on space to move the cursor; long-press space = trackpad (lines too); slide from `.?123` onto a symbol and release
|
- Slide sideways on space to move the cursor; long-press space = trackpad (lines too); slide from `.?123` onto a symbol and release
|
||||||
- Compact (iPad) or full (iPad Pro) keyboard: number row, punctuation, tab, caps lock, ctrl/alt/super, dead keys
|
- Compact (iPad) or full (iPad Pro) keyboard: number row, punctuation, tab, caps lock, ctrl/alt/super/alt gr, dead keys
|
||||||
- Ctrl/Alt/Super stick like shift (tap, double-tap, hold) and turn keys into shortcuts; a tapped or held shift joins them (Shift+F5, Shift+Tab, Shift+arrows), auto-shift and caps lock don't
|
- Ctrl/Alt/Super stick like shift (tap, double-tap, hold) and turn keys into shortcuts; a tapped or held shift joins them (Shift+F5, Shift+Tab, Shift+arrows), auto-shift and caps lock don't
|
||||||
|
- Alt Gr (right of space in full mode) types the keymap's AltGr symbols (sk: Alt Gr+J = '), sticky like the others
|
||||||
- Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙)
|
- Optional PC key blocks: function row, navigation (ins/home/pg up, del/end/pg dn, arrows), numeric keypad; any mix, typing keys included (⚙)
|
||||||
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
|
- Floating: drag the bottom bar, resize from the corner; geometry is remembered per screen
|
||||||
- Follows Plasma's virtual keyboard setting (tray toggle): off never pops up on focus, the default only after touch or a pen; "Always Show" and D-Bus `Show`/`Toggle` still show it
|
- Follows Plasma's virtual keyboard setting (tray toggle): off never pops up on focus, the default only after touch or a pen; "Always Show" and D-Bus `Show`/`Toggle` still show it
|
||||||
@@ -46,7 +47,7 @@ Dev hooks: `SLOPOSK_AUTOTYPE=text` commits on focus; `SLOPOSK_SCREENSHOT=file.pn
|
|||||||
|
|
||||||
Letters come from the system keymap (what KWin sends to every client): its Q/A/Z rows become the three letter rows. Letters elsewhere, e.g. Slovak ľščťž on the number row, become long-press alternates. The globe key switches the system layout over `org.kde.keyboard`, so the physical keyboard follows.
|
Letters come from the system keymap (what KWin sends to every client): its Q/A/Z rows become the three letter rows. Letters elsewhere, e.g. Slovak ľščťž on the number row, become long-press alternates. The globe key switches the system layout over `org.kde.keyboard`, so the physical keyboard follows.
|
||||||
|
|
||||||
In full mode every typing key comes from the keymap (number row, `[ ] \ ; ' , . /` or the layout's equivalents), with the shifted symbol shown above. Dead keys compose with the locale's XKB compose table.
|
In full mode every typing key comes from the keymap (number row, `[ ] \ ; ' , . /` or the layout's equivalents), with the shifted symbol shown above (and flickable); letters, whose shift only capitalizes, show their AltGr symbol instead. Dead keys compose with the locale's XKB compose table.
|
||||||
|
|
||||||
Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.json` replaces it.
|
Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.json` replaces it.
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ Everything else is `layouts/template.json`; `~/.local/share/sloposk/template.jso
|
|||||||
- `keysym`: sends the XKB keysym named by `keysym` (`Home`, `F5`, `Left`...) on press; `repeat` auto-repeats while held
|
- `keysym`: sends the XKB keysym named by `keysym` (`Home`, `F5`, `Left`...) on press; `repeat` auto-repeats while held
|
||||||
- `letters`: the keymap's letters in row `row` (0–2), with `flicks` handed out left to right
|
- `letters`: the keymap's letters in row `row` (0–2), with `flicks` handed out left to right
|
||||||
- `keys`: every typing key in keymap row `row` (0 = number row … 3 = bottom)
|
- `keys`: every typing key in keymap row `row` (0 = number row … 3 = bottom)
|
||||||
- `modifier`: `"modifier": "ctrl" | "alt" | "super"`; `capslock`
|
- `modifier`: `"modifier": "ctrl" | "alt" | "super" | "altgr"`; `capslock`
|
||||||
- Layer `full`: rows used instead of `rows` in full-keyboard mode
|
- Layer `full`: rows used instead of `rows` in full-keyboard mode
|
||||||
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
- `stretch`: the key grows so its row is as wide as the layer's widest (letter counts vary by layout)
|
||||||
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
- Top-level `alternates`: long-press choices per letter, after the keymap's own
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
{"type": "modifier", "modifier": "alt", "label": "alt", "width": 1.25},
|
{"type": "modifier", "modifier": "alt", "label": "alt", "width": 1.25},
|
||||||
{"type": "globe"},
|
{"type": "globe"},
|
||||||
{"type": "space", "width": 6, "stretch": true},
|
{"type": "space", "width": 6, "stretch": true},
|
||||||
{"type": "modifier", "modifier": "alt", "label": "alt", "width": 1.25},
|
{"type": "modifier", "modifier": "altgr", "label": "alt gr", "width": 1.25},
|
||||||
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.25},
|
{"type": "layer", "label": ".?123", "target": "numbers", "width": 1.25},
|
||||||
{"type": "dismiss"}
|
{"type": "dismiss"}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ constexpr uint32_t DestroyVersion = 5;
|
|||||||
constexpr uint32_t Released = 0;
|
constexpr uint32_t Released = 0;
|
||||||
constexpr uint32_t Pressed = 1;
|
constexpr uint32_t Pressed = 1;
|
||||||
|
|
||||||
|
constexpr uint32_t RightAlt = 100; // KEY_RIGHTALT
|
||||||
|
|
||||||
// Modifiers are held with real keys (evdev codes) so they combine with any keysym.
|
// Modifiers are held with real keys (evdev codes) so they combine with any keysym.
|
||||||
struct ModifierKey {
|
struct ModifierKey {
|
||||||
Qt::KeyboardModifier flag;
|
Qt::KeyboardModifier flag;
|
||||||
@@ -65,9 +67,16 @@ void KWinFakeInput::sendKeysym(quint32 keysym, Qt::KeyboardModifiers modifiers)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KWin resets the modifier state after each keysym except Shift, Ctrl, Alt
|
||||||
|
// and Super, so a held AltGr keysym would never shift the next key: hold
|
||||||
|
// the physical key instead (AltGr is right Alt on the layouts that have it).
|
||||||
void KWinFakeInput::setKeyPressed(quint32 keysym, bool pressed)
|
void KWinFakeInput::setKeyPressed(quint32 keysym, bool pressed)
|
||||||
{
|
{
|
||||||
if (canSendKeysyms())
|
if (!canSendKeysyms())
|
||||||
|
return;
|
||||||
|
if (keysym == XKB_KEY_ISO_Level3_Shift)
|
||||||
|
keyboard_key(RightAlt, pressed ? Pressed : Released);
|
||||||
|
else
|
||||||
keyboard_keysym(keysym, pressed ? Pressed : Released);
|
keyboard_keysym(keysym, pressed ? Pressed : Released);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ quint32 modifierKeysym(const QString &name)
|
|||||||
return XKB_KEY_Control_L;
|
return XKB_KEY_Control_L;
|
||||||
if (name == u"alt")
|
if (name == u"alt")
|
||||||
return XKB_KEY_Alt_L;
|
return XKB_KEY_Alt_L;
|
||||||
|
if (name == u"altgr")
|
||||||
|
return XKB_KEY_ISO_Level3_Shift;
|
||||||
return XKB_KEY_Super_L;
|
return XKB_KEY_Super_L;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,17 +82,18 @@ bool HardwareKeys::isShiftHeld() const
|
|||||||
return m_down.contains(XKB_KEY_Shift_L);
|
return m_down.contains(XKB_KEY_Shift_L);
|
||||||
}
|
}
|
||||||
|
|
||||||
ShiftLevel HardwareKeys::shiftLevel() const
|
KeyLevel HardwareKeys::keyLevel() const
|
||||||
{
|
{
|
||||||
|
const bool altGr = m_down.contains(XKB_KEY_ISO_Level3_Shift);
|
||||||
if (isShiftHeld())
|
if (isShiftHeld())
|
||||||
return ShiftLevel::Shifted;
|
return {ShiftLevel::Shifted, altGr};
|
||||||
return m_capsLock ? ShiftLevel::Capitals : ShiftLevel::None;
|
return {m_capsLock ? ShiftLevel::Capitals : ShiftLevel::None, altGr};
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap HardwareKeys::modifierModes() const
|
QVariantMap HardwareKeys::modifierModes() const
|
||||||
{
|
{
|
||||||
QVariantMap modes;
|
QVariantMap modes;
|
||||||
for (const char *name : {"ctrl", "alt", "super"}) {
|
for (const char *name : {"ctrl", "alt", "super", "altgr"}) {
|
||||||
const QString key = QString::fromLatin1(name);
|
const QString key = QString::fromLatin1(name);
|
||||||
modes.insert(key, m_down.contains(modifierKeysym(key)) ? 1 : 0);
|
modes.insert(key, m_down.contains(modifierKeysym(key)) ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class InputEngine;
|
|||||||
|
|
||||||
// The "hold keys" mode: the keyboard acts like a hardware one. A key is down
|
// The "hold keys" mode: the keyboard acts like a hardware one. A key is down
|
||||||
// while the finger is, so apps see held keys (games, Shift+click, the
|
// while the finger is, so apps see held keys (games, Shift+click, the
|
||||||
// system's key repeat). Shift, Ctrl, Alt and Super are plain keys, caps lock
|
// system's key repeat). Shift, Ctrl, Alt, Super and AltGr are plain keys, caps lock
|
||||||
// is the system's, and dead keys compose in the app; none of the touch
|
// is the system's, and dead keys compose in the app; none of the touch
|
||||||
// conveniences (sticky shift, auto-capitalization, accents) apply.
|
// conveniences (sticky shift, auto-capitalization, accents) apply.
|
||||||
class HardwareKeys
|
class HardwareKeys
|
||||||
@@ -25,11 +25,11 @@ 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 shown while held, caps lock while on.
|
// For the view: shift and AltGr shown while held, caps lock while on.
|
||||||
ShiftLevel shiftLevel() const;
|
KeyLevel keyLevel() const;
|
||||||
bool isShiftHeld() const;
|
bool isShiftHeld() const;
|
||||||
bool isCapsLockOn() const { return m_capsLock; }
|
bool isCapsLockOn() const { return m_capsLock; }
|
||||||
// "ctrl"/"alt"/"super" → 1 while held (ShiftState::Mode::Once), else 0.
|
// "ctrl"/"alt"/"super"/"altgr" → 1 while held (ShiftState::Mode::Once), else 0.
|
||||||
QVariantMap modifierModes() const;
|
QVariantMap modifierModes() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ bool KeyboardController::holdKey(const KeyDef &key, bool pressed)
|
|||||||
if (!m_options.holdKeys || !HardwareKeys::handles(key))
|
if (!m_options.holdKeys || !HardwareKeys::handles(key))
|
||||||
return false;
|
return false;
|
||||||
pressed ? m_hardware.press(key) : m_hardware.release(key);
|
pressed ? m_hardware.press(key) : m_hardware.release(key);
|
||||||
m_model.setShiftLevel(shiftLevel());
|
m_model.setKeyLevel(keyLevel());
|
||||||
Q_EMIT shiftChanged();
|
Q_EMIT shiftChanged();
|
||||||
Q_EMIT modifiersChanged();
|
Q_EMIT modifiersChanged();
|
||||||
return true;
|
return true;
|
||||||
@@ -68,7 +68,7 @@ void KeyboardController::keyDown(const KeyDef &key, qint64 timeMs)
|
|||||||
break;
|
break;
|
||||||
case KeyType::Modifier:
|
case KeyType::Modifier:
|
||||||
m_modifiers.press(key.modifier, timeMs);
|
m_modifiers.press(key.modifier, timeMs);
|
||||||
Q_EMIT modifiersChanged();
|
modifiersUpdated();
|
||||||
break;
|
break;
|
||||||
case KeyType::Backspace:
|
case KeyType::Backspace:
|
||||||
if (m_deadKeys.isPending()) {
|
if (m_deadKeys.isPending()) {
|
||||||
@@ -110,7 +110,7 @@ void KeyboardController::keyUp(const KeyDef &key, KeyOutput output, const QStrin
|
|||||||
break;
|
break;
|
||||||
case KeyType::Modifier:
|
case KeyType::Modifier:
|
||||||
m_modifiers.release(key.modifier);
|
m_modifiers.release(key.modifier);
|
||||||
Q_EMIT modifiersChanged();
|
modifiersUpdated();
|
||||||
break;
|
break;
|
||||||
case KeyType::Backspace:
|
case KeyType::Backspace:
|
||||||
m_backspaceRepeater.stop();
|
m_backspaceRepeater.stop();
|
||||||
@@ -147,7 +147,7 @@ void KeyboardController::keyCancelled(const KeyDef &key)
|
|||||||
changeShift([&] { m_shift.release(); });
|
changeShift([&] { m_shift.release(); });
|
||||||
else if (key.type == KeyType::Modifier) {
|
else if (key.type == KeyType::Modifier) {
|
||||||
m_modifiers.release(key.modifier);
|
m_modifiers.release(key.modifier);
|
||||||
Q_EMIT modifiersChanged();
|
modifiersUpdated();
|
||||||
} else if (key.type == KeyType::Backspace)
|
} else if (key.type == KeyType::Backspace)
|
||||||
m_backspaceRepeater.stop();
|
m_backspaceRepeater.stop();
|
||||||
else if (key.type == KeyType::Keysym)
|
else if (key.type == KeyType::Keysym)
|
||||||
@@ -210,7 +210,7 @@ void KeyboardController::setOptions(const TypingOptions &options)
|
|||||||
m_shift = {};
|
m_shift = {};
|
||||||
}
|
}
|
||||||
m_options = options;
|
m_options = options;
|
||||||
m_model.setShiftLevel(shiftLevel());
|
m_model.setKeyLevel(keyLevel());
|
||||||
Q_EMIT shiftChanged();
|
Q_EMIT shiftChanged();
|
||||||
Q_EMIT modifiersChanged();
|
Q_EMIT modifiersChanged();
|
||||||
autoCapitalize();
|
autoCapitalize();
|
||||||
@@ -227,7 +227,12 @@ void KeyboardController::typeCharacter(const KeyDef &key, KeyOutput output, cons
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShiftLevel level = shiftLevel();
|
const KeyLevel level = keyLevel();
|
||||||
|
if (level.altGr) {
|
||||||
|
// Used up by this key, like a one-shot shift.
|
||||||
|
m_modifiers.keyTyped();
|
||||||
|
modifiersUpdated();
|
||||||
|
}
|
||||||
if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) {
|
if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) {
|
||||||
if (const QString done = m_deadKeys.press(dead); !done.isEmpty())
|
if (const QString done = m_deadKeys.press(dead); !done.isEmpty())
|
||||||
typeText(done);
|
typeText(done);
|
||||||
@@ -260,7 +265,7 @@ Qt::KeyboardModifiers KeyboardController::shortcutModifiers() const
|
|||||||
void KeyboardController::useModifiers()
|
void KeyboardController::useModifiers()
|
||||||
{
|
{
|
||||||
m_modifiers.keyTyped();
|
m_modifiers.keyTyped();
|
||||||
Q_EMIT modifiersChanged();
|
modifiersUpdated();
|
||||||
if (m_shift.appliesToShortcuts())
|
if (m_shift.appliesToShortcuts())
|
||||||
changeShift([&] { m_shift.characterTyped(); });
|
changeShift([&] { m_shift.characterTyped(); });
|
||||||
}
|
}
|
||||||
@@ -309,23 +314,30 @@ void KeyboardController::autoCapitalize()
|
|||||||
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
changeShift([&] { m_shift.setAutomatic(capitalize); });
|
||||||
}
|
}
|
||||||
|
|
||||||
ShiftLevel KeyboardController::shiftLevel() const
|
KeyLevel KeyboardController::keyLevel() const
|
||||||
{
|
{
|
||||||
if (m_options.holdKeys)
|
if (m_options.holdKeys)
|
||||||
return m_hardware.shiftLevel();
|
return m_hardware.keyLevel();
|
||||||
|
const bool altGr = m_modifiers.isAltGrActive();
|
||||||
if (m_shift.appliesToShortcuts())
|
if (m_shift.appliesToShortcuts())
|
||||||
return ShiftLevel::Shifted;
|
return {ShiftLevel::Shifted, altGr};
|
||||||
return m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None;
|
return {m_shift.isActive() ? ShiftLevel::Capitals : ShiftLevel::None, altGr};
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyboardController::modifiersUpdated()
|
||||||
|
{
|
||||||
|
m_model.setKeyLevel(keyLevel()); // AltGr changes the labels
|
||||||
|
Q_EMIT modifiersChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename Change>
|
template<typename Change>
|
||||||
void KeyboardController::changeShift(Change change)
|
void KeyboardController::changeShift(Change change)
|
||||||
{
|
{
|
||||||
const ShiftState::Mode mode = m_shift.mode();
|
const ShiftState::Mode mode = m_shift.mode();
|
||||||
const ShiftLevel level = shiftLevel();
|
const KeyLevel level = keyLevel();
|
||||||
change();
|
change();
|
||||||
if (m_shift.mode() == mode && shiftLevel() == level)
|
if (m_shift.mode() == mode && keyLevel() == level)
|
||||||
return;
|
return;
|
||||||
m_model.setShiftLevel(shiftLevel());
|
m_model.setKeyLevel(keyLevel());
|
||||||
Q_EMIT shiftChanged();
|
Q_EMIT shiftChanged();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ enum class KeyOutput { Primary, Flick, Alternate };
|
|||||||
// receives key presses and text-field context, and talks to an InputEngine.
|
// receives key presses and text-field context, and talks to an InputEngine.
|
||||||
//
|
//
|
||||||
// With Ctrl, Alt or Super active, keys become shortcuts (keysyms with those
|
// With Ctrl, Alt or Super active, keys become shortcuts (keysyms with those
|
||||||
// modifiers) instead of text. Shift joins shortcuts and non-text keys
|
// modifiers) instead of text; AltGr instead types a key's AltGr symbol. Shift joins shortcuts and non-text keys
|
||||||
// (Shift+F5, Shift+Tab, Shift+arrows) only when the user turned it on, not
|
// (Shift+F5, Shift+Tab, Shift+arrows) only when the user turned it on, not
|
||||||
// when auto-capitalization did.
|
// when auto-capitalization did.
|
||||||
class KeyboardController : public QObject
|
class KeyboardController : public QObject
|
||||||
@@ -34,7 +34,7 @@ class KeyboardController : public QObject
|
|||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
QML_UNCREATABLE("Created in C++")
|
QML_UNCREATABLE("Created in C++")
|
||||||
Q_PROPERTY(ShiftMode shiftMode READ shiftMode NOTIFY shiftChanged)
|
Q_PROPERTY(ShiftMode shiftMode READ shiftMode NOTIFY shiftChanged)
|
||||||
// "ctrl"/"alt"/"super" → ShiftMode
|
// "ctrl"/"alt"/"super"/"altgr" → ShiftMode
|
||||||
Q_PROPERTY(QVariantMap modifierModes READ modifierModes NOTIFY modifiersChanged)
|
Q_PROPERTY(QVariantMap modifierModes READ modifierModes NOTIFY modifiersChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -78,10 +78,12 @@ private:
|
|||||||
// 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 uses them up.
|
||||||
void sendKey(quint32 keysym);
|
void sendKey(quint32 keysym);
|
||||||
Qt::KeyboardModifiers shortcutModifiers() const;
|
Qt::KeyboardModifiers shortcutModifiers() const;
|
||||||
ShiftLevel shiftLevel() 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; false if it isn't one it presses.
|
||||||
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.
|
||||||
|
void modifiersUpdated();
|
||||||
void deleteBackward(bool wholeWord);
|
void deleteBackward(bool wholeWord);
|
||||||
void sendRepeatedKeysym();
|
void sendRepeatedKeysym();
|
||||||
void stopRepeating(const KeyDef &key);
|
void stopRepeating(const KeyDef &key);
|
||||||
|
|||||||
@@ -40,7 +40,12 @@ Qt::KeyboardModifiers Modifiers::active() const
|
|||||||
if (entry.state.isActive())
|
if (entry.state.isActive())
|
||||||
flags |= entry.flag;
|
flags |= entry.flag;
|
||||||
}
|
}
|
||||||
return flags;
|
return flags & ~Qt::GroupSwitchModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Modifiers::isAltGrActive() const
|
||||||
|
{
|
||||||
|
return m_entries.back().state.isActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap Modifiers::modes() const
|
QVariantMap Modifiers::modes() const
|
||||||
|
|||||||
@@ -8,19 +8,22 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
// Ctrl, Alt and Super, each sticky like shift (see ShiftState): tap for the
|
// Ctrl, Alt, Super and AltGr, each sticky like shift (see ShiftState): tap
|
||||||
// next key, double-tap to lock, hold while typing with another finger.
|
// for the next key, double-tap to lock, hold while typing with another finger.
|
||||||
|
// The first three turn keys into shortcuts; AltGr picks their AltGr symbols.
|
||||||
class Modifiers
|
class Modifiers
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Names as used in layouts: "ctrl", "alt", "super".
|
// 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.
|
// A key was typed: one-shot modifiers are used up.
|
||||||
void keyTyped();
|
void keyTyped();
|
||||||
void clear();
|
void clear();
|
||||||
|
|
||||||
|
// Ctrl, Alt and Super: what makes a key a shortcut.
|
||||||
Qt::KeyboardModifiers active() const;
|
Qt::KeyboardModifiers active() const;
|
||||||
|
bool isAltGrActive() const;
|
||||||
// name → ShiftState::Mode as int, for the view.
|
// name → ShiftState::Mode as int, for the view.
|
||||||
QVariantMap modes() const;
|
QVariantMap modes() const;
|
||||||
|
|
||||||
@@ -32,9 +35,10 @@ private:
|
|||||||
};
|
};
|
||||||
ShiftState *find(const QString &name);
|
ShiftState *find(const QString &name);
|
||||||
|
|
||||||
std::array<Entry, 3> m_entries{{
|
std::array<Entry, 4> m_entries{{
|
||||||
{"ctrl", Qt::ControlModifier, {}},
|
{"ctrl", Qt::ControlModifier, {}},
|
||||||
{"alt", Qt::AltModifier, {}},
|
{"alt", Qt::AltModifier, {}},
|
||||||
{"super", Qt::MetaModifier, {}},
|
{"super", Qt::MetaModifier, {}},
|
||||||
|
{"altgr", Qt::GroupSwitchModifier, {}},
|
||||||
}};
|
}};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ std::optional<KeymapKey> keyAt(xkb_keymap *keymap, int code, xkb_layout_index_t
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
if (!readLevel(symbolAt(keymap, code, layout, 1), key.upper, key.deadUpper))
|
if (!readLevel(symbolAt(keymap, code, layout, 1), key.upper, key.deadUpper))
|
||||||
key.upper = key.lower.toUpper();
|
key.upper = key.lower.toUpper();
|
||||||
|
readLevel(symbolAt(keymap, code, layout, 2), key.altGr, key.deadAltGr);
|
||||||
|
readLevel(symbolAt(keymap, code, layout, 3), key.altGrUpper, key.deadAltGrUpper);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
struct xkb_keymap;
|
struct xkb_keymap;
|
||||||
|
|
||||||
// One typing key of a keymap layout, at its two shift levels. A level is
|
// One typing key of a keymap layout, at its shift and AltGr levels. A level
|
||||||
// either text or a dead key (then `text` is its spacing accent, for labels).
|
// is either text or a dead key (then the text is its spacing accent, for labels).
|
||||||
struct KeymapKey {
|
struct KeymapKey {
|
||||||
enum class Position { Regular, Backslash, LessGreater }; // BKSL, LSGT: not on every keyboard
|
enum class Position { Regular, Backslash, LessGreater }; // BKSL, LSGT: not on every keyboard
|
||||||
|
|
||||||
@@ -15,6 +15,10 @@ struct KeymapKey {
|
|||||||
QString upper;
|
QString upper;
|
||||||
quint32 deadLower = 0; // dead keysym, 0 if the level types text
|
quint32 deadLower = 0; // dead keysym, 0 if the level types text
|
||||||
quint32 deadUpper = 0;
|
quint32 deadUpper = 0;
|
||||||
|
QString altGr; // third level (AltGr), empty if it types nothing
|
||||||
|
QString altGrUpper; // fourth level (AltGr+Shift)
|
||||||
|
quint32 deadAltGr = 0;
|
||||||
|
quint32 deadAltGrUpper = 0;
|
||||||
|
|
||||||
bool isLetter() const;
|
bool isLetter() const;
|
||||||
};
|
};
|
||||||
|
|||||||
+33
-4
@@ -27,6 +27,14 @@ enum class KeyType {
|
|||||||
// (Shift+1 = !), as on a PC.
|
// (Shift+1 = !), as on a PC.
|
||||||
enum class ShiftLevel { None, Capitals, Shifted };
|
enum class ShiftLevel { None, Capitals, Shifted };
|
||||||
|
|
||||||
|
// Shift plus AltGr, which picks a key's third (and, shifted, fourth) keymap level.
|
||||||
|
struct KeyLevel {
|
||||||
|
KeyLevel(ShiftLevel shift = ShiftLevel::None, bool altGr = false) : shift(shift), altGr(altGr) {}
|
||||||
|
ShiftLevel shift;
|
||||||
|
bool altGr;
|
||||||
|
friend bool operator==(const KeyLevel &, const KeyLevel &) = default;
|
||||||
|
};
|
||||||
|
|
||||||
struct KeyDef {
|
struct KeyDef {
|
||||||
KeyType type = KeyType::Character;
|
KeyType type = KeyType::Character;
|
||||||
QString label; // shown and typed (for character keys)
|
QString label; // shown and typed (for character keys)
|
||||||
@@ -37,6 +45,10 @@ struct KeyDef {
|
|||||||
QString modifier; // Modifier only: "ctrl", "alt" or "super"
|
QString modifier; // Modifier only: "ctrl", "alt" or "super"
|
||||||
quint32 dead = 0; // character keys: dead keysym typed instead of `label`, 0 if none
|
quint32 dead = 0; // character keys: dead keysym typed instead of `label`, 0 if none
|
||||||
quint32 deadShifted = 0; // same for the shifted level
|
quint32 deadShifted = 0; // same for the shifted level
|
||||||
|
QString altGr; // keymap keys: typed with AltGr, empty if nothing
|
||||||
|
QString altGrShifted; // with AltGr and shift, empty if nothing
|
||||||
|
quint32 deadAltGr = 0; // dead keysyms of those levels (then the texts are their accents)
|
||||||
|
quint32 deadAltGrShifted = 0;
|
||||||
qreal width = 1.0; // in key units
|
qreal width = 1.0; // in key units
|
||||||
bool stretch = false; // grows so the row is as wide as its block's widest
|
bool stretch = false; // grows so the row is as wide as its block's widest
|
||||||
quint32 keysym = 0; // Keysym keys only
|
quint32 keysym = 0; // Keysym keys only
|
||||||
@@ -47,17 +59,34 @@ struct KeyDef {
|
|||||||
QStringList flicks; // flick symbol per letter, left to right
|
QStringList flicks; // flick symbol per letter, left to right
|
||||||
|
|
||||||
// Text this key shows/types; only character keys change.
|
// Text this key shows/types; only character keys change.
|
||||||
QString text(ShiftLevel level) const
|
// Without an AltGr symbol, AltGr leaves the key as it is (as on a PC).
|
||||||
|
QString text(KeyLevel level) const
|
||||||
{
|
{
|
||||||
if (level == ShiftLevel::None || type != KeyType::Character)
|
if (type != KeyType::Character)
|
||||||
return label;
|
return label;
|
||||||
if (level == ShiftLevel::Capitals)
|
if (usesAltGr(level))
|
||||||
|
return usesAltGrShifted(level) ? altGrShifted : altGr;
|
||||||
|
if (level.shift == ShiftLevel::None)
|
||||||
|
return label;
|
||||||
|
if (level.shift == ShiftLevel::Capitals)
|
||||||
return isLetter() ? label.toUpper() : label;
|
return isLetter() ? label.toUpper() : label;
|
||||||
return shifted.isEmpty() ? label.toUpper() : shifted;
|
return shifted.isEmpty() ? label.toUpper() : shifted;
|
||||||
}
|
}
|
||||||
// The dead keysym typed at `level`, 0 if it types text.
|
// The dead keysym typed at `level`, 0 if it types text.
|
||||||
quint32 deadAt(ShiftLevel level) const { return level == ShiftLevel::Shifted ? deadShifted : dead; }
|
quint32 deadAt(KeyLevel level) const
|
||||||
|
{
|
||||||
|
if (usesAltGr(level))
|
||||||
|
return usesAltGrShifted(level) ? deadAltGrShifted : deadAltGr;
|
||||||
|
return level.shift == ShiftLevel::Shifted ? deadShifted : dead;
|
||||||
|
}
|
||||||
bool isLetter() const { return !dead && label.size() == 1 && label.front().isLetter(); }
|
bool isLetter() const { return !dead && label.size() == 1 && label.front().isLetter(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool usesAltGr(KeyLevel level) const { return level.altGr && !altGr.isEmpty(); }
|
||||||
|
bool usesAltGrShifted(KeyLevel level) const
|
||||||
|
{
|
||||||
|
return level.shift == ShiftLevel::Shifted && !altGrShifted.isEmpty();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
using Row = QList<KeyDef>;
|
using Row = QList<KeyDef>;
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ KeyDef characterKey(const KeymapKey &source)
|
|||||||
key.shifted = source.upper;
|
key.shifted = source.upper;
|
||||||
key.dead = source.deadLower;
|
key.dead = source.deadLower;
|
||||||
key.deadShifted = source.deadUpper;
|
key.deadShifted = source.deadUpper;
|
||||||
|
key.altGr = source.altGr;
|
||||||
|
key.altGrShifted = source.altGrUpper;
|
||||||
|
key.deadAltGr = source.deadAltGr;
|
||||||
|
key.deadAltGrShifted = source.deadAltGrUpper;
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +58,8 @@ 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.
|
// A full row: every typing key, the shifted symbol shown (and flickable) above;
|
||||||
|
// for letters, whose shift only capitalizes, the AltGr symbol (sk: ' on j).
|
||||||
// `withLessGreater` tells whether the ISO key left of Z is among them.
|
// `withLessGreater` tells whether the ISO key left of Z is among them.
|
||||||
QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
|
QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
|
||||||
{
|
{
|
||||||
@@ -76,6 +81,8 @@ QList<KeyDef> rowKeys(const KeymapKeys &keymap, int row, bool *withLessGreater)
|
|||||||
// Show (and flick for) a shifted level that isn't just the capital letter: ! on 1, " on sk ô.
|
// Show (and flick for) a shifted level that isn't 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)
|
||||||
|
key.flick = source.altGr;
|
||||||
keys.append(key);
|
keys.append(key);
|
||||||
}
|
}
|
||||||
return keys;
|
return keys;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace {
|
|||||||
|
|
||||||
constexpr int LetterRows = 3;
|
constexpr int LetterRows = 3;
|
||||||
constexpr int KeyRows = 4;
|
constexpr int KeyRows = 4;
|
||||||
const QStringList ModifierNames = {QStringLiteral("ctrl"), QStringLiteral("alt"), QStringLiteral("super")};
|
const QStringList ModifierNames = {QStringLiteral("ctrl"), QStringLiteral("alt"), QStringLiteral("super"),
|
||||||
|
QStringLiteral("altgr")};
|
||||||
|
|
||||||
QStringList toStringList(const QJsonValue &value)
|
QStringList toStringList(const QJsonValue &value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ QVariant LayoutModel::data(const QModelIndex &index, int role) const
|
|||||||
const KeyDef &key = m_keys.at(index.row());
|
const KeyDef &key = m_keys.at(index.row());
|
||||||
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_shiftLevel);
|
case LabelRole: return key.text(m_keyLevel);
|
||||||
// A shifted key already shows its shifted symbol (! on 1): don't repeat it.
|
// A shifted or AltGr key may already show its flick symbol (! on 1): don't repeat it.
|
||||||
case FlickLabelRole: return m_shiftLevel == ShiftLevel::Shifted && key.flick == key.shifted ? 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);
|
||||||
case KeyXRole: return rect.x();
|
case KeyXRole: return rect.x();
|
||||||
@@ -133,11 +133,11 @@ void LayoutModel::setSize(QSizeF size)
|
|||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {KeyXRole, KeyYRole, KeyWidthRole, KeyHeightRole});
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {KeyXRole, KeyYRole, KeyWidthRole, KeyHeightRole});
|
||||||
}
|
}
|
||||||
|
|
||||||
void LayoutModel::setShiftLevel(ShiftLevel level)
|
void LayoutModel::setKeyLevel(KeyLevel level)
|
||||||
{
|
{
|
||||||
if (level == m_shiftLevel)
|
if (level == m_keyLevel)
|
||||||
return;
|
return;
|
||||||
m_shiftLevel = level;
|
m_keyLevel = level;
|
||||||
if (!m_keys.isEmpty())
|
if (!m_keys.isEmpty())
|
||||||
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {LabelRole, FlickLabelRole});
|
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {LabelRole, FlickLabelRole});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public:
|
|||||||
// Section ids to show; MainSectionId stands for the layers.
|
// Section ids to show; MainSectionId stands for the layers.
|
||||||
void setVisibleSections(const QStringList &ids);
|
void setVisibleSections(const QStringList &ids);
|
||||||
void setSize(QSizeF size);
|
void setSize(QSizeF size);
|
||||||
void setShiftLevel(ShiftLevel level);
|
void setKeyLevel(KeyLevel level);
|
||||||
void setPressed(int index, bool pressed);
|
void setPressed(int index, bool pressed);
|
||||||
void setFlickProgress(int index, qreal progress);
|
void setFlickProgress(int index, qreal progress);
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ public:
|
|||||||
int keyAt(QPointF pos) const;
|
int keyAt(QPointF pos) const;
|
||||||
const KeyDef &key(int index) const { return m_keys.at(index); }
|
const KeyDef &key(int index) const { return m_keys.at(index); }
|
||||||
QRectF keyRect(int index) const { return m_rects.value(index); }
|
QRectF keyRect(int index) const { return m_rects.value(index); }
|
||||||
bool isUppercase() const { return m_shiftLevel != ShiftLevel::None; }
|
bool isUppercase() const { return m_keyLevel.shift != ShiftLevel::None; }
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void layoutChanged();
|
void layoutChanged();
|
||||||
@@ -75,7 +75,7 @@ private:
|
|||||||
QStringList m_visibleSections{MainSectionId};
|
QStringList m_visibleSections{MainSectionId};
|
||||||
QList<Block> m_blocks;
|
QList<Block> m_blocks;
|
||||||
QSizeF m_size;
|
QSizeF m_size;
|
||||||
ShiftLevel m_shiftLevel = ShiftLevel::None;
|
KeyLevel m_keyLevel;
|
||||||
|
|
||||||
QList<KeyDef> m_keys;
|
QList<KeyDef> m_keys;
|
||||||
QList<QRectF> m_rects;
|
QList<QRectF> m_rects;
|
||||||
|
|||||||
@@ -328,6 +328,29 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(controller->modifierModes().value(u"alt"_s).toInt(), int(KeyboardController::ShiftMode::Off));
|
QCOMPARE(controller->modifierModes().value(u"alt"_s).toInt(), int(KeyboardController::ShiftMode::Off));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void altGrTypesTheThirdLevel()
|
||||||
|
{
|
||||||
|
KeyDef j = makeKey(KeyType::Character, u"j"_s);
|
||||||
|
j.altGr = u"'"_s;
|
||||||
|
tap(modifierKey(u"altgr"_s));
|
||||||
|
tap(j);
|
||||||
|
QCOMPARE(engine.output, u"'");
|
||||||
|
QVERIFY(engine.keysyms.isEmpty()); // text, not an Alt shortcut
|
||||||
|
tap(j); // one-shot
|
||||||
|
QCOMPARE(engine.output, u"'j");
|
||||||
|
}
|
||||||
|
|
||||||
|
void altGrDeadKeyComposes()
|
||||||
|
{
|
||||||
|
KeyDef three = makeKey(KeyType::Character, u"3"_s);
|
||||||
|
three.altGr = u"^"_s;
|
||||||
|
three.deadAltGr = XKB_KEY_dead_circumflex;
|
||||||
|
tap(modifierKey(u"altgr"_s));
|
||||||
|
tap(three);
|
||||||
|
tap(makeKey(KeyType::Character, u"a"_s));
|
||||||
|
QCOMPARE(engine.output, u"â");
|
||||||
|
}
|
||||||
|
|
||||||
void tappedShiftAppliesToFunctionKeys()
|
void tappedShiftAppliesToFunctionKeys()
|
||||||
{
|
{
|
||||||
tap(makeKey(KeyType::Character, u"a"_s)); // mid-sentence: no auto-shift
|
tap(makeKey(KeyType::Character, u"a"_s)); // mid-sentence: no auto-shift
|
||||||
|
|||||||
@@ -137,6 +137,19 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(letterKey(sk, u"l"_s).alternates.first(), u"ľ");
|
QCOMPARE(letterKey(sk, u"l"_s).alternates.first(), u"ľ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void fullModeLettersFlickToTheirAltGrSymbol()
|
||||||
|
{
|
||||||
|
const Layout sk = TestLayouts::build("sk", KeyboardMode::Full);
|
||||||
|
const KeyDef j = letterKey(sk, u"j"_s);
|
||||||
|
QCOMPARE(j.altGr, u"'");
|
||||||
|
QCOMPARE(j.flick, u"'");
|
||||||
|
QCOMPARE(j.text({ShiftLevel::None, true}), u"'");
|
||||||
|
QCOMPARE(letterKey(sk, u"ô"_s).flick, u"\""); // keys with a shifted symbol keep it
|
||||||
|
const KeyDef plus = letterKey(sk, u"+"_s);
|
||||||
|
QCOMPARE(plus.deadAt({ShiftLevel::Shifted, true}), quint32(XKB_KEY_dead_tilde));
|
||||||
|
QCOMPARE(plus.text({ShiftLevel::None, true}), u"!");
|
||||||
|
}
|
||||||
|
|
||||||
void noPlaceholdersRemain()
|
void noPlaceholdersRemain()
|
||||||
{
|
{
|
||||||
for (const Layer &layer : TestLayouts::build("us").layers) {
|
for (const Layer &layer : TestLayouts::build("us").layers) {
|
||||||
|
|||||||
Reference in New Issue
Block a user