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:
@@ -37,5 +37,13 @@ struct TextContext {
|
||||
return field && prosePurpose && !(hints & (HiddenText | SensitiveData));
|
||||
}
|
||||
|
||||
bool allowsAutoCapitalization() const { return isProse() && !(hints & Lowercase); }
|
||||
// Fields that state any text preference must ask for capitals; most that
|
||||
// state none (hints 0) still want them.
|
||||
bool allowsAutoCapitalization() const
|
||||
{
|
||||
constexpr quint32 Preferences = AutoCompletion | AutoCorrection | AutoCapitalization | Lowercase
|
||||
| Uppercase | Titlecase;
|
||||
const bool asked = (hints & AutoCapitalization) || !(hints & Preferences);
|
||||
return isProse() && asked && !(hints & Lowercase);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ void ShiftState::press(qint64 timeMs)
|
||||
m_held = true;
|
||||
m_typedWhileHeld = false;
|
||||
m_automatic = false;
|
||||
m_userChoice = true;
|
||||
|
||||
switch (m_mode) {
|
||||
case Mode::Off: m_mode = doubleTap ? Mode::Locked : Mode::Once; break;
|
||||
@@ -28,6 +29,7 @@ void ShiftState::release()
|
||||
void ShiftState::characterTyped()
|
||||
{
|
||||
m_lastPress = NoPress; // shift, type, shift is not a double tap
|
||||
m_userChoice = false;
|
||||
if (m_held)
|
||||
m_typedWhileHeld = true;
|
||||
else if (m_mode == Mode::Once)
|
||||
@@ -36,8 +38,13 @@ void ShiftState::characterTyped()
|
||||
|
||||
void ShiftState::setAutomatic(bool capitalize)
|
||||
{
|
||||
if (m_held || m_mode == Mode::Locked)
|
||||
if (m_held)
|
||||
return;
|
||||
const bool changed = capitalize != m_hint;
|
||||
m_hint = capitalize;
|
||||
if (m_mode == Mode::Locked || (m_userChoice && !changed))
|
||||
return;
|
||||
m_userChoice = false;
|
||||
m_mode = capitalize ? Mode::Once : Mode::Off;
|
||||
m_automatic = capitalize;
|
||||
}
|
||||
@@ -46,4 +53,5 @@ void ShiftState::toggleLock()
|
||||
{
|
||||
m_mode = m_mode == Mode::Locked ? Mode::Off : Mode::Locked;
|
||||
m_automatic = false;
|
||||
m_userChoice = true;
|
||||
}
|
||||
|
||||
@@ -27,14 +27,20 @@ public:
|
||||
void release();
|
||||
void characterTyped();
|
||||
|
||||
// Auto-capitalization hint; ignored while shift is held or locked.
|
||||
// Auto-capitalization hint; ignored while shift is held or locked. A shift
|
||||
// the user tapped stays until a character is typed or the hint changes,
|
||||
// so a field re-reporting the same context doesn't undo it.
|
||||
void setAutomatic(bool capitalize);
|
||||
// A new field: the user's shift choice no longer applies.
|
||||
void forgetUserChoice() { m_userChoice = false; }
|
||||
// Caps lock key: locks, or unlocks if locked.
|
||||
void toggleLock();
|
||||
|
||||
private:
|
||||
Mode m_mode = Mode::Off;
|
||||
bool m_automatic = false;
|
||||
bool m_hint = false; // last setAutomatic value
|
||||
bool m_userChoice = false; // the user set the mode since the last character
|
||||
bool m_held = false;
|
||||
bool m_typedWhileHeld = false;
|
||||
bool m_lastPressUnlocked = false; // a tap leaving caps lock doesn't start a double tap
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "TextRules.h"
|
||||
|
||||
#include <QTextBoundaryFinder>
|
||||
|
||||
namespace TextRules {
|
||||
|
||||
namespace {
|
||||
@@ -38,10 +40,9 @@ bool wantsDoubleSpacePeriod(const QString &textBeforeCursor)
|
||||
|
||||
QString withoutLastCharacter(const QString &text)
|
||||
{
|
||||
if (text.isEmpty())
|
||||
return text;
|
||||
const bool pair = text.size() >= 2 && text.back().isLowSurrogate() && text[text.size() - 2].isHighSurrogate();
|
||||
return text.chopped(pair ? 2 : 1);
|
||||
QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme, text);
|
||||
finder.toEnd();
|
||||
return text.left(qMax<qsizetype>(0, finder.toPreviousBoundary()));
|
||||
}
|
||||
|
||||
QString withoutLastWord(const QString &text)
|
||||
|
||||
@@ -12,7 +12,8 @@ bool startsSentence(const QString &textBeforeCursor);
|
||||
// True if typing another space should turn "word " into "word. ".
|
||||
bool wantsDoubleSpacePeriod(const QString &textBeforeCursor);
|
||||
|
||||
// `text` without its last character (surrogate pairs count as one).
|
||||
// `text` without its last character, as Backspace deletes it: a whole
|
||||
// grapheme cluster (é as e + accent, emoji sequences).
|
||||
QString withoutLastCharacter(const QString &text);
|
||||
|
||||
// `text` without its trailing word and the whitespace after it,
|
||||
|
||||
Reference in New Issue
Block a user