Keep the user's shift; trust the text guess less

- A tapped shift (on or off) survives the field re-reporting its context:
  it lasts until a character is typed, the hint changes or a field is focused
- An input-method reset forgets the guessed text and pending dead key, so
  double-space can't delete a character the keyboard never saw
- Fields that state typing preferences only get capitals if they ask
- Trackpad moves in fields without surrounding text make the guess unknown;
  Backspace removes a whole grapheme cluster from it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:13:27 +02:00
co-authored by Claude Opus 5.5
parent 076bc14e2f
commit d44aab7772
9 changed files with 130 additions and 10 deletions
+15 -2
View File
@@ -14,6 +14,9 @@ static_assert(int(KeyboardController::ShiftMode::Locked) == int(ShiftState::Mode
namespace {
// Enough history for the text rules; fields may report megabytes.
constexpr qsizetype MaxTextBefore = 256;
// Stands for text we can't see: neither a sentence start nor a word, so no
// rule fires on it. For fields that don't report text, after cursor moves.
constexpr QChar UnknownText = u'\uFFFC';
// Like a hardware key: no word mode, a faster rate than Backspace.
constexpr KeyRepeater::Timing KeysymRepeat{.intervalMs = 50, .wordModeAfterMs = std::numeric_limits<int>::max()};
}
@@ -168,20 +171,30 @@ void KeyboardController::moveCursor(QPoint steps)
repeat(qMax(0, -steps.x()), XKB_KEY_Left);
repeat(qMax(0, steps.y()), XKB_KEY_Down);
repeat(qMax(0, -steps.y()), XKB_KEY_Up);
if (!steps.isNull() && !m_context.hasSurroundingText) {
m_textBefore = UnknownText;
autoCapitalize();
}
}
void KeyboardController::resetContext(const TextContext &context)
{
m_deadKeys.cancel();
m_textBefore.clear();
m_shift.forgetUserChoice();
setContext(context);
}
void KeyboardController::setContext(const TextContext &context)
{
m_context = context;
if (context.hasSurroundingText)
if (context.hasSurroundingText) {
m_textBefore = context.textBeforeCursor.right(MaxTextBefore);
} else if (m_context.hasSurroundingText) {
// The field stopped reporting (an input-method reset): what we knew is stale.
m_deadKeys.cancel();
m_textBefore.clear();
}
m_context = context;
autoCapitalize();
}