Skip to content

Commit 073cefa

Browse files
authored
[RawKeyboard] Fix Linux remapped CapsLock throws (#115009)
Co-authored-by: Bruno Leroux <[email protected]>
1 parent 567d004 commit 073cefa

File tree

3 files changed

+95
-36
lines changed

3 files changed

+95
-36
lines changed

packages/flutter/lib/src/services/raw_keyboard.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -824,9 +824,18 @@ class RawKeyboard {
824824
modifierKeys[physicalModifier] = _allModifiers[physicalModifier]!;
825825
}
826826
}
827-
_allModifiersExceptFn.keys
828-
.where((PhysicalKeyboardKey key) => !anySideKeys.contains(key))
829-
.forEach(_keysPressed.remove);
827+
// On Linux, CapsLock key can be mapped to a non-modifier logical key:
828+
// https://github.com/flutter/flutter/issues/114591.
829+
// This is also affecting Flutter Web on Linux.
830+
final bool nonModifierCapsLock = (event.data is RawKeyEventDataLinux || event.data is RawKeyEventDataWeb)
831+
&& _keysPressed[PhysicalKeyboardKey.capsLock] != null
832+
&& _keysPressed[PhysicalKeyboardKey.capsLock] != LogicalKeyboardKey.capsLock;
833+
for (final PhysicalKeyboardKey physicalKey in _allModifiersExceptFn.keys) {
834+
final bool skipReleasingKey = nonModifierCapsLock && physicalKey == PhysicalKeyboardKey.capsLock;
835+
if (!anySideKeys.contains(physicalKey) && !skipReleasingKey) {
836+
_keysPressed.remove(physicalKey);
837+
}
838+
}
830839
if (event.data is! RawKeyEventDataFuchsia && event.data is! RawKeyEventDataMacOs) {
831840
// On Fuchsia and macOS, the Fn key is not considered a modifier key.
832841
_keysPressed.remove(PhysicalKeyboardKey.fn);

packages/flutter/lib/src/services/raw_keyboard_web.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ class RawKeyEventDataWeb extends RawKeyEventData {
106106
return maybeLocationKey;
107107
}
108108

109-
// Look to see if the [code] is one we know about and have a mapping for.
110-
final LogicalKeyboardKey? newKey = kWebToLogicalKey[code];
109+
// Look to see if the [key] is one we know about and have a mapping for.
110+
final LogicalKeyboardKey? newKey = kWebToLogicalKey[key];
111111
if (newKey != null) {
112112
return newKey;
113113
}

packages/flutter/test/services/raw_keyboard_test.dart

Lines changed: 81 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -350,45 +350,45 @@ void main() {
350350
);
351351
}, skip: isBrowser); // [intended] This is a GLFW-specific test.
352352

353+
Future<void> simulateGTKKeyEvent(bool keyDown, int scancode, int keycode, int modifiers) async {
354+
final Map<String, dynamic> data = <String, dynamic>{
355+
'type': keyDown ? 'keydown' : 'keyup',
356+
'keymap': 'linux',
357+
'toolkit': 'gtk',
358+
'scanCode': scancode,
359+
'keyCode': keycode,
360+
'modifiers': modifiers,
361+
};
362+
// Dispatch an empty key data to disable HardwareKeyboard sanity check,
363+
// since we're only testing if the raw keyboard can handle the message.
364+
// In a real application, the embedder responder will send the correct key data
365+
// (which is tested in the engine).
366+
TestDefaultBinaryMessengerBinding.instance!.keyEventManager.handleKeyData(const ui.KeyData(
367+
type: ui.KeyEventType.down,
368+
timeStamp: Duration.zero,
369+
logical: 0,
370+
physical: 0,
371+
character: null,
372+
synthesized: false,
373+
));
374+
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
375+
SystemChannels.keyEvent.name,
376+
SystemChannels.keyEvent.codec.encodeMessage(data),
377+
(ByteData? data) {},
378+
);
379+
}
353380

354381
// Regression test for https://github.com/flutter/flutter/issues/93278 .
355382
//
356383
// GTK has some weird behavior where the tested key event sequence will
357384
// result in a AltRight down event without Alt bitmask.
358385
testWidgets('keysPressed modifiers are synchronized with key events on Linux GTK (down events)', (WidgetTester tester) async {
359386
expect(RawKeyboard.instance.keysPressed, isEmpty);
360-
Future<void> simulate(bool keyDown, int scancode, int keycode, int modifiers) async {
361-
final Map<String, dynamic> data = <String, dynamic>{
362-
'type': keyDown ? 'keydown' : 'keyup',
363-
'keymap': 'linux',
364-
'toolkit': 'gtk',
365-
'scanCode': scancode,
366-
'keyCode': keycode,
367-
'modifiers': modifiers,
368-
};
369-
// Dispatch an empty key data to disable HardwareKeyboard sanity check,
370-
// since we're only testing if the raw keyboard can handle the message.
371-
// In real application the embedder responder will send correct key data
372-
// (which is tested in the engine.)
373-
TestDefaultBinaryMessengerBinding.instance!.keyEventManager.handleKeyData(const ui.KeyData(
374-
type: ui.KeyEventType.down,
375-
timeStamp: Duration.zero,
376-
logical: 0,
377-
physical: 0,
378-
character: null,
379-
synthesized: false,
380-
));
381-
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
382-
SystemChannels.keyEvent.name,
383-
SystemChannels.keyEvent.codec.encodeMessage(data),
384-
(ByteData? data) {},
385-
);
386-
}
387387

388-
await simulate(true, 0x6c/*AltRight*/, 0xffea/*AltRight*/, 0x2000000);
389-
await simulate(true, 0x32/*ShiftLeft*/, 0xfe08/*NextGroup*/, 0x2000008/*MOD3*/);
390-
await simulate(false, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002008/*MOD3|Reserve14*/);
391-
await simulate(true, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002000/*Reserve14*/);
388+
await simulateGTKKeyEvent(true, 0x6c/*AltRight*/, 0xffea/*AltRight*/, 0x2000000);
389+
await simulateGTKKeyEvent(true, 0x32/*ShiftLeft*/, 0xfe08/*NextGroup*/, 0x2000008/*MOD3*/);
390+
await simulateGTKKeyEvent(false, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002008/*MOD3|Reserve14*/);
391+
await simulateGTKKeyEvent(true, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002000/*Reserve14*/);
392392
expect(
393393
RawKeyboard.instance.keysPressed,
394394
equals(
@@ -399,6 +399,56 @@ void main() {
399399
);
400400
}, skip: isBrowser); // [intended] This is a GTK-specific test.
401401

402+
// Regression test for https://github.com/flutter/flutter/issues/114591 .
403+
//
404+
// On Linux, CapsLock can be remapped to a non-modifier key.
405+
testWidgets('CapsLock should not be release when remapped on Linux', (WidgetTester tester) async {
406+
expect(RawKeyboard.instance.keysPressed, isEmpty);
407+
408+
await simulateGTKKeyEvent(true, 0x42/*CapsLock*/, 0xff08/*Backspace*/, 0x2000000);
409+
expect(
410+
RawKeyboard.instance.keysPressed,
411+
equals(
412+
<LogicalKeyboardKey>{
413+
LogicalKeyboardKey.backspace,
414+
},
415+
),
416+
);
417+
}, skip: isBrowser); // [intended] This is a GTK-specific test.
418+
419+
// Regression test for https://github.com/flutter/flutter/issues/114591 .
420+
//
421+
// On Web, CapsLock can be remapped to a non-modifier key.
422+
testWidgets('CapsLock should not be release when remapped on Web', (WidgetTester _) async {
423+
final List<RawKeyEvent> events = <RawKeyEvent>[];
424+
RawKeyboard.instance.addListener(events.add);
425+
addTearDown(() {
426+
RawKeyboard.instance.removeListener(events.add);
427+
});
428+
await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage(
429+
SystemChannels.keyEvent.name,
430+
SystemChannels.keyEvent.codec.encodeMessage(const <String, dynamic>{
431+
'type': 'keydown',
432+
'keymap': 'web',
433+
'code': 'CapsLock',
434+
'key': 'Backspace',
435+
'location': 0,
436+
'metaState': 0,
437+
'keyCode': 8,
438+
}),
439+
(ByteData? data) { },
440+
);
441+
442+
expect(
443+
RawKeyboard.instance.keysPressed,
444+
equals(
445+
<LogicalKeyboardKey>{
446+
LogicalKeyboardKey.backspace,
447+
},
448+
),
449+
);
450+
}, skip: !isBrowser); // [intended] This is a Browser-specific test.
451+
402452
testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async {
403453
expect(RawKeyboard.instance.keysPressed, isEmpty);
404454
// Generate the data for a regular key down event. Change the modifiers so

0 commit comments

Comments
 (0)