require a clean release for double-tap modifier detection (T-409)

Typing Shift+; opened quick-open instead of a colon. Two flaws in the
T-341 detector: it counted a tap on the Shift keydown (so a chorded
press could complete the gesture before the chord key arrived), and it
relied on the chorded key bubbling to the root KeyboardListener to
break the gesture — but a focused editor or text field consumes that
event, so the tracker never saw it.

The tracker now models press/release: a tap is a press with no other
key going down while the modifier is held, and the gesture fires on
the second clean release. The root shell feeds it from a
HardwareKeyboard handler, which observes every event before focus
dispatch regardless of who consumes it, and treats a modifier pressed
while a non-modifier is already held as a chord.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 08:49:20 +02:00
co-authored by Claude Fable 5
parent 70d71f2a3a
commit 8966a159db
7 changed files with 264 additions and 59 deletions
+38
View File
@@ -354,6 +354,44 @@ void main() {
expect(tester.takeException(), isNull);
});
testWidgets('double-tapped bare Shift opens quick-open (T-341)', (tester) async {
await pumpApp(tester);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.pump();
expect(f.services.quickOpen.isOpen, isTrue);
expect(tester.takeException(), isNull);
});
testWidgets('typing colons (Shift+;) never triggers quick-open (T-409)', (tester) async {
await pumpApp(tester);
// Two rapid `:` keystrokes — the chorded `;` dirties each Shift press.
for (var i = 0; i < 2; i++) {
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.semicolon);
await tester.sendKeyUpEvent(LogicalKeyboardKey.semicolon);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
}
await tester.pump();
expect(f.services.quickOpen.isOpen, isFalse);
expect(tester.takeException(), isNull);
});
testWidgets('a bare Shift tap followed by a Shift chord does not fire (T-409)', (tester) async {
await pumpApp(tester);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); // clean tap arms
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.semicolon); // chord — old code fired on the down
await tester.sendKeyUpEvent(LogicalKeyboardKey.semicolon);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft);
await tester.pump();
expect(f.services.quickOpen.isOpen, isFalse);
expect(tester.takeException(), isNull);
});
testWidgets('file.closeWorkspace command closes the active project', (tester) async {
final repo = Directory.current.path;
await tester.runAsync(() async => f.services.project.open(repo));