Dead keys: act on compose status at press time

- Two dead keys that compose (´´ → ´) type the result right away; a dead
  key that can't continue the sequence types the old accents and starts anew
- With Ctrl/Alt/Super, a dead key sends its dead keysym, as the physical
  key does, not the spacing accent from its label

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:13:49 +02:00
co-authored by Claude Opus 5.5
parent 9abd19e5d3
commit 536fc12d2f
5 changed files with 64 additions and 15 deletions
+29 -7
View File
@@ -37,11 +37,36 @@ DeadKeys::DeadKeys(const QString &locale)
DeadKeys::~DeadKeys() = default; DeadKeys::~DeadKeys() = default;
void DeadKeys::press(quint32 deadKeysym) QString DeadKeys::press(quint32 deadKeysym)
{ {
if (!m_state) {
m_pending += deadKeyText(deadKeysym); m_pending += deadKeyText(deadKeysym);
if (m_state) return {};
}
xkb_compose_state_feed(m_state.get(), deadKeysym); xkb_compose_state_feed(m_state.get(), deadKeysym);
switch (xkb_compose_state_get_status(m_state.get())) {
case XKB_COMPOSE_COMPOSED: {
const QString composed = composedText();
cancel();
return composed;
}
case XKB_COMPOSE_CANCELLED: {
// No sequence goes on like this: type the old accents, start anew.
const QString accents = m_pending;
cancel();
return accents + press(deadKeysym);
}
default:
m_pending += deadKeyText(deadKeysym);
return {};
}
}
QString DeadKeys::composedText() const
{
char utf8[64];
xkb_compose_state_get_utf8(m_state.get(), utf8, sizeof utf8);
return QString::fromUtf8(utf8);
} }
QString DeadKeys::compose(const QString &text) QString DeadKeys::compose(const QString &text)
@@ -50,11 +75,8 @@ QString DeadKeys::compose(const QString &text)
if (m_state && !text.isEmpty()) { if (m_state && !text.isEmpty()) {
const char32_t ucs = text.toUcs4().first(); const char32_t ucs = text.toUcs4().first();
xkb_compose_state_feed(m_state.get(), xkb_utf32_to_keysym(ucs)); xkb_compose_state_feed(m_state.get(), xkb_utf32_to_keysym(ucs));
if (xkb_compose_state_get_status(m_state.get()) == XKB_COMPOSE_COMPOSED) { if (xkb_compose_state_get_status(m_state.get()) == XKB_COMPOSE_COMPOSED)
char utf8[64]; result = composedText() + text.mid(QString::fromUcs4(&ucs, 1).size());
xkb_compose_state_get_utf8(m_state.get(), utf8, sizeof utf8);
result = QString::fromUtf8(utf8) + text.mid(QString::fromUcs4(&ucs, 1).size());
}
} }
cancel(); cancel();
return result; return result;
+6 -2
View File
@@ -17,14 +17,18 @@ 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. // A dead key was typed: starts or extends the sequence. Returns text to
void press(quint32 deadKeysym); // type now: what two dead keys composed (´´ → ´), or the accents of a
// sequence the new one can't continue; empty while still pending.
QString press(quint32 deadKeysym);
// `text` typed while pending: the composed character, or the accents // `text` typed while pending: the composed character, or the accents
// followed by `text` when nothing composes. Ends the sequence. // followed by `text` when nothing composes. Ends the sequence.
QString compose(const QString &text); QString compose(const QString &text);
void cancel(); void cancel();
private: private:
QString composedText() const;
std::unique_ptr<xkb_context, void (*)(xkb_context *)> m_context; std::unique_ptr<xkb_context, void (*)(xkb_context *)> m_context;
std::unique_ptr<xkb_compose_table, void (*)(xkb_compose_table *)> m_table; std::unique_ptr<xkb_compose_table, void (*)(xkb_compose_table *)> m_table;
std::unique_ptr<xkb_compose_state, void (*)(xkb_compose_state *)> m_state; std::unique_ptr<xkb_compose_state, void (*)(xkb_compose_state *)> m_state;
+6 -3
View File
@@ -220,15 +220,18 @@ 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. // A shortcut such as Ctrl+C: the key's unshifted symbol, with modifiers;
// for a dead key its dead keysym, as the physical key sends.
m_deadKeys.cancel(); m_deadKeys.cancel();
sendKey(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;
} }
const ShiftLevel level = shiftLevel(); const ShiftLevel level = shiftLevel();
if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) { if (const quint32 dead = key.deadAt(level); dead && output == KeyOutput::Primary) {
m_deadKeys.press(dead); if (const QString done = m_deadKeys.press(dead); !done.isEmpty())
typeText(done);
else
changeShift([&] { m_shift.characterTyped(); }); changeShift([&] { m_shift.characterTyped(); });
return; return;
} }
+10
View File
@@ -373,6 +373,16 @@ private Q_SLOTS:
QCOMPARE(engine.output, u"+Ľ2"); QCOMPARE(engine.output, u"+Ľ2");
} }
void ctrlWithDeadKeySendsTheDeadKeysym()
{
KeyDef acute = makeKey(KeyType::Character, u"´"_s);
acute.dead = XKB_KEY_dead_acute;
tap(modifierKey(u"ctrl"_s));
tap(acute);
QCOMPARE(engine.keysyms, QList<quint32>{XKB_KEY_dead_acute});
QCOMPARE(engine.lastModifiers, Qt::ControlModifier);
}
void deadKeyComposesWithNextCharacter() void deadKeyComposesWithNextCharacter()
{ {
KeyDef acute = makeKey(KeyType::Character, u"´"_s); KeyDef acute = makeKey(KeyType::Character, u"´"_s);
+10
View File
@@ -30,6 +30,16 @@ private Q_SLOTS:
QCOMPARE(dead.compose(u"q"_s), u"ˇq"); QCOMPARE(dead.compose(u"q"_s), u"ˇq");
} }
void twoDeadKeysComposeAtOnce()
{
DeadKeys dead(u"en_US.UTF-8"_s);
QVERIFY(dead.press(XKB_KEY_dead_acute).isEmpty());
QCOMPARE(dead.press(XKB_KEY_dead_acute), u"´");
QVERIFY(!dead.isPending());
dead.press(XKB_KEY_dead_acute); // a fresh sequence
QCOMPARE(dead.compose(u"e"_s), u"é");
}
void cancelForgetsTheAccent() void cancelForgetsTheAccent()
{ {
DeadKeys dead(u"en_US.UTF-8"_s); DeadKeys dead(u"en_US.UTF-8"_s);