From 073cefad021dae0bf70f1929cfa4bbf9431fe46c Mon Sep 17 00:00:00 2001 From: Bruno Leroux Date: Mon, 21 Nov 2022 20:43:55 +0100 Subject: [PATCH] [RawKeyboard] Fix Linux remapped CapsLock throws (#115009) Co-authored-by: Bruno Leroux --- .../lib/src/services/raw_keyboard.dart | 15 ++- .../lib/src/services/raw_keyboard_web.dart | 4 +- .../test/services/raw_keyboard_test.dart | 112 +++++++++++++----- 3 files changed, 95 insertions(+), 36 deletions(-) diff --git a/packages/flutter/lib/src/services/raw_keyboard.dart b/packages/flutter/lib/src/services/raw_keyboard.dart index 4f5a0a4d646..e7789eb17fe 100644 --- a/packages/flutter/lib/src/services/raw_keyboard.dart +++ b/packages/flutter/lib/src/services/raw_keyboard.dart @@ -824,9 +824,18 @@ class RawKeyboard { modifierKeys[physicalModifier] = _allModifiers[physicalModifier]!; } } - _allModifiersExceptFn.keys - .where((PhysicalKeyboardKey key) => !anySideKeys.contains(key)) - .forEach(_keysPressed.remove); + // On Linux, CapsLock key can be mapped to a non-modifier logical key: + // https://github.com/flutter/flutter/issues/114591. + // This is also affecting Flutter Web on Linux. + final bool nonModifierCapsLock = (event.data is RawKeyEventDataLinux || event.data is RawKeyEventDataWeb) + && _keysPressed[PhysicalKeyboardKey.capsLock] != null + && _keysPressed[PhysicalKeyboardKey.capsLock] != LogicalKeyboardKey.capsLock; + for (final PhysicalKeyboardKey physicalKey in _allModifiersExceptFn.keys) { + final bool skipReleasingKey = nonModifierCapsLock && physicalKey == PhysicalKeyboardKey.capsLock; + if (!anySideKeys.contains(physicalKey) && !skipReleasingKey) { + _keysPressed.remove(physicalKey); + } + } if (event.data is! RawKeyEventDataFuchsia && event.data is! RawKeyEventDataMacOs) { // On Fuchsia and macOS, the Fn key is not considered a modifier key. _keysPressed.remove(PhysicalKeyboardKey.fn); diff --git a/packages/flutter/lib/src/services/raw_keyboard_web.dart b/packages/flutter/lib/src/services/raw_keyboard_web.dart index 3abf1dddc21..3ad7a6fb3fd 100644 --- a/packages/flutter/lib/src/services/raw_keyboard_web.dart +++ b/packages/flutter/lib/src/services/raw_keyboard_web.dart @@ -106,8 +106,8 @@ class RawKeyEventDataWeb extends RawKeyEventData { return maybeLocationKey; } - // Look to see if the [code] is one we know about and have a mapping for. - final LogicalKeyboardKey? newKey = kWebToLogicalKey[code]; + // Look to see if the [key] is one we know about and have a mapping for. + final LogicalKeyboardKey? newKey = kWebToLogicalKey[key]; if (newKey != null) { return newKey; } diff --git a/packages/flutter/test/services/raw_keyboard_test.dart b/packages/flutter/test/services/raw_keyboard_test.dart index d73dbb163df..88b1cb7b606 100644 --- a/packages/flutter/test/services/raw_keyboard_test.dart +++ b/packages/flutter/test/services/raw_keyboard_test.dart @@ -350,6 +350,33 @@ void main() { ); }, skip: isBrowser); // [intended] This is a GLFW-specific test. + Future simulateGTKKeyEvent(bool keyDown, int scancode, int keycode, int modifiers) async { + final Map data = { + 'type': keyDown ? 'keydown' : 'keyup', + 'keymap': 'linux', + 'toolkit': 'gtk', + 'scanCode': scancode, + 'keyCode': keycode, + 'modifiers': modifiers, + }; + // Dispatch an empty key data to disable HardwareKeyboard sanity check, + // since we're only testing if the raw keyboard can handle the message. + // In a real application, the embedder responder will send the correct key data + // (which is tested in the engine). + TestDefaultBinaryMessengerBinding.instance!.keyEventManager.handleKeyData(const ui.KeyData( + type: ui.KeyEventType.down, + timeStamp: Duration.zero, + logical: 0, + physical: 0, + character: null, + synthesized: false, + )); + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(data), + (ByteData? data) {}, + ); + } // Regression test for https://github.com/flutter/flutter/issues/93278 . // @@ -357,38 +384,11 @@ void main() { // result in a AltRight down event without Alt bitmask. testWidgets('keysPressed modifiers are synchronized with key events on Linux GTK (down events)', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); - Future simulate(bool keyDown, int scancode, int keycode, int modifiers) async { - final Map data = { - 'type': keyDown ? 'keydown' : 'keyup', - 'keymap': 'linux', - 'toolkit': 'gtk', - 'scanCode': scancode, - 'keyCode': keycode, - 'modifiers': modifiers, - }; - // Dispatch an empty key data to disable HardwareKeyboard sanity check, - // since we're only testing if the raw keyboard can handle the message. - // In real application the embedder responder will send correct key data - // (which is tested in the engine.) - TestDefaultBinaryMessengerBinding.instance!.keyEventManager.handleKeyData(const ui.KeyData( - type: ui.KeyEventType.down, - timeStamp: Duration.zero, - logical: 0, - physical: 0, - character: null, - synthesized: false, - )); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( - SystemChannels.keyEvent.name, - SystemChannels.keyEvent.codec.encodeMessage(data), - (ByteData? data) {}, - ); - } - await simulate(true, 0x6c/*AltRight*/, 0xffea/*AltRight*/, 0x2000000); - await simulate(true, 0x32/*ShiftLeft*/, 0xfe08/*NextGroup*/, 0x2000008/*MOD3*/); - await simulate(false, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002008/*MOD3|Reserve14*/); - await simulate(true, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002000/*Reserve14*/); + await simulateGTKKeyEvent(true, 0x6c/*AltRight*/, 0xffea/*AltRight*/, 0x2000000); + await simulateGTKKeyEvent(true, 0x32/*ShiftLeft*/, 0xfe08/*NextGroup*/, 0x2000008/*MOD3*/); + await simulateGTKKeyEvent(false, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002008/*MOD3|Reserve14*/); + await simulateGTKKeyEvent(true, 0x6c/*AltRight*/, 0xfe03/*AltRight*/, 0x2002000/*Reserve14*/); expect( RawKeyboard.instance.keysPressed, equals( @@ -399,6 +399,56 @@ void main() { ); }, skip: isBrowser); // [intended] This is a GTK-specific test. + // Regression test for https://github.com/flutter/flutter/issues/114591 . + // + // On Linux, CapsLock can be remapped to a non-modifier key. + testWidgets('CapsLock should not be release when remapped on Linux', (WidgetTester tester) async { + expect(RawKeyboard.instance.keysPressed, isEmpty); + + await simulateGTKKeyEvent(true, 0x42/*CapsLock*/, 0xff08/*Backspace*/, 0x2000000); + expect( + RawKeyboard.instance.keysPressed, + equals( + { + LogicalKeyboardKey.backspace, + }, + ), + ); + }, skip: isBrowser); // [intended] This is a GTK-specific test. + + // Regression test for https://github.com/flutter/flutter/issues/114591 . + // + // On Web, CapsLock can be remapped to a non-modifier key. + testWidgets('CapsLock should not be release when remapped on Web', (WidgetTester _) async { + final List events = []; + RawKeyboard.instance.addListener(events.add); + addTearDown(() { + RawKeyboard.instance.removeListener(events.add); + }); + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + SystemChannels.keyEvent.name, + SystemChannels.keyEvent.codec.encodeMessage(const { + 'type': 'keydown', + 'keymap': 'web', + 'code': 'CapsLock', + 'key': 'Backspace', + 'location': 0, + 'metaState': 0, + 'keyCode': 8, + }), + (ByteData? data) { }, + ); + + expect( + RawKeyboard.instance.keysPressed, + equals( + { + LogicalKeyboardKey.backspace, + }, + ), + ); + }, skip: !isBrowser); // [intended] This is a Browser-specific test. + testWidgets('keysPressed modifiers are synchronized with key events on web', (WidgetTester tester) async { expect(RawKeyboard.instance.keysPressed, isEmpty); // Generate the data for a regular key down event. Change the modifiers so