Unverified Commit 028ed712 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Synchronize modifier keys in RawKeyboard.keysPressed with modifier flags on events. (#43948)

Currently, we listen to keyboard events to find out which keys should be represented in RawKeyboard.instance.keysPressed, but that's not sufficient to represent the physical state of the keys, since modifier keys could have been pressed when the overall app did not have keyboard focus (especially on desktop platforms).

This PR synchronizes the list of modifier keys in keysPressed with the modifier key flags that are present in the raw key event so that they can be relied upon to represent the current state of the keyboard. When synchronizing these states, we don't send any new key events, since they didn't happen when the app had keyboard focus, but if you ask "is this key down", we'll give the right answer
parent 3243ebe3
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
...@@ -295,8 +297,8 @@ abstract class RawKeyEvent extends Diagnosticable { ...@@ -295,8 +297,8 @@ abstract class RawKeyEvent extends Diagnosticable {
); );
break; break;
default: default:
// We don't yet implement raw key events on iOS or other platforms, but // Raw key events are not yet implemented on iOS or other platforms,
// we don't hit this exception because the engine never sends us these // but this exception isn't hit, because the engine never sends these
// messages. // messages.
throw FlutterError('Unknown keymap for key events: $keymap'); throw FlutterError('Unknown keymap for key events: $keymap');
} }
...@@ -506,6 +508,9 @@ class RawKeyboard { ...@@ -506,6 +508,9 @@ class RawKeyboard {
if (event is RawKeyUpEvent) { if (event is RawKeyUpEvent) {
_keysPressed.remove(event.logicalKey); _keysPressed.remove(event.logicalKey);
} }
// Make sure that the modifiers reflect reality, in case a modifier key was
// pressed/released while the app didn't have focus.
_synchronizeModifiers(event);
if (_listeners.isEmpty) { if (_listeners.isEmpty) {
return; return;
} }
...@@ -516,6 +521,68 @@ class RawKeyboard { ...@@ -516,6 +521,68 @@ class RawKeyboard {
} }
} }
static final Map<_ModifierSidePair, Set<LogicalKeyboardKey>> _modifierKeyMap = <_ModifierSidePair, Set<LogicalKeyboardKey>>{
const _ModifierSidePair(ModifierKey.altModifier, KeyboardSide.left): <LogicalKeyboardKey>{LogicalKeyboardKey.altLeft},
const _ModifierSidePair(ModifierKey.altModifier, KeyboardSide.right): <LogicalKeyboardKey>{LogicalKeyboardKey.altRight},
const _ModifierSidePair(ModifierKey.altModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.altLeft, LogicalKeyboardKey.altRight},
const _ModifierSidePair(ModifierKey.altModifier, KeyboardSide.any): <LogicalKeyboardKey>{LogicalKeyboardKey.altLeft},
const _ModifierSidePair(ModifierKey.shiftModifier, KeyboardSide.left): <LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft},
const _ModifierSidePair(ModifierKey.shiftModifier, KeyboardSide.right): <LogicalKeyboardKey>{LogicalKeyboardKey.shiftRight},
const _ModifierSidePair(ModifierKey.shiftModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft, LogicalKeyboardKey.shiftRight},
const _ModifierSidePair(ModifierKey.shiftModifier, KeyboardSide.any): <LogicalKeyboardKey>{LogicalKeyboardKey.shiftLeft},
const _ModifierSidePair(ModifierKey.controlModifier, KeyboardSide.left): <LogicalKeyboardKey>{LogicalKeyboardKey.controlLeft},
const _ModifierSidePair(ModifierKey.controlModifier, KeyboardSide.right): <LogicalKeyboardKey>{LogicalKeyboardKey.controlRight},
const _ModifierSidePair(ModifierKey.controlModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.controlLeft, LogicalKeyboardKey.controlRight},
const _ModifierSidePair(ModifierKey.controlModifier, KeyboardSide.any): <LogicalKeyboardKey>{LogicalKeyboardKey.controlLeft},
const _ModifierSidePair(ModifierKey.metaModifier, KeyboardSide.left): <LogicalKeyboardKey>{LogicalKeyboardKey.metaLeft},
const _ModifierSidePair(ModifierKey.metaModifier, KeyboardSide.right): <LogicalKeyboardKey>{LogicalKeyboardKey.metaRight},
const _ModifierSidePair(ModifierKey.metaModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.metaLeft, LogicalKeyboardKey.metaRight},
const _ModifierSidePair(ModifierKey.metaModifier, KeyboardSide.any): <LogicalKeyboardKey>{LogicalKeyboardKey.metaLeft},
const _ModifierSidePair(ModifierKey.capsLockModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.capsLock},
const _ModifierSidePair(ModifierKey.numLockModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.numLock},
const _ModifierSidePair(ModifierKey.scrollLockModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.scrollLock},
const _ModifierSidePair(ModifierKey.functionModifier, KeyboardSide.all): <LogicalKeyboardKey>{LogicalKeyboardKey.fn},
// The symbolModifier doesn't have a key representation on any of the
// platforms, so don't map it here.
};
// The list of all modifier keys that are represented in modifier key bit
// masks on all platforms, so that they can be cleared out of pressedKeys when
// synchronizing.
static final Set<LogicalKeyboardKey> _allModifiers = <LogicalKeyboardKey>{
LogicalKeyboardKey.altLeft,
LogicalKeyboardKey.altRight,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.shiftRight,
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.controlRight,
LogicalKeyboardKey.metaLeft,
LogicalKeyboardKey.metaRight,
LogicalKeyboardKey.capsLock,
LogicalKeyboardKey.numLock,
LogicalKeyboardKey.scrollLock,
LogicalKeyboardKey.fn,
};
void _synchronizeModifiers(RawKeyEvent event) {
final Map<ModifierKey, KeyboardSide> modifiersPressed = event.data.modifiersPressed;
final Set<LogicalKeyboardKey> modifierKeys = <LogicalKeyboardKey>{};
for (ModifierKey key in modifiersPressed.keys) {
final Set<LogicalKeyboardKey> mappedKeys = _modifierKeyMap[_ModifierSidePair(key, modifiersPressed[key])];
assert(mappedKeys != null,
'Platform key support for ${Platform.operatingSystem} is '
'producing unsupported modifier combinations.');
modifierKeys.addAll(mappedKeys);
}
// Don't send any key events for these changes, since there *should* be
// separate events for each modifier key down/up that occurs while the app
// has focus. This is just to synchronize the modifier keys when they are
// pressed/released while the app doesn't have focus, to make sure that
// _keysPressed reflects reality at all times.
_keysPressed.removeAll(_allModifiers);
_keysPressed.addAll(modifierKeys);
}
final Set<LogicalKeyboardKey> _keysPressed = <LogicalKeyboardKey>{}; final Set<LogicalKeyboardKey> _keysPressed = <LogicalKeyboardKey>{};
/// Returns the set of keys currently pressed. /// Returns the set of keys currently pressed.
...@@ -531,3 +598,20 @@ class RawKeyboard { ...@@ -531,3 +598,20 @@ class RawKeyboard {
_keysPressed.clear(); _keysPressed.clear();
} }
} }
class _ModifierSidePair extends Object {
const _ModifierSidePair(this.modifier, this.side);
final ModifierKey modifier;
final KeyboardSide side;
@override
bool operator ==(dynamic other) {
return runtimeType == other.runtimeType
&& modifier == other.modifier
&& side == other.side;
}
@override
int get hashCode => hashValues(modifier, side);
}
...@@ -113,8 +113,8 @@ class RawKeyEventDataMacOs extends RawKeyEventData { ...@@ -113,8 +113,8 @@ class RawKeyEventDataMacOs extends RawKeyEventData {
); );
} }
// This is a non-printable key that we don't know about, so we mint a new // This is a non-printable key that is unrecognized, so a new code is minted
// code with the autogenerated bit set. // with the autogenerated bit set.
const int macOsKeyIdPlane = 0x00500000000; const int macOsKeyIdPlane = 0x00500000000;
return LogicalKeyboardKey( return LogicalKeyboardKey(
...@@ -154,13 +154,15 @@ class RawKeyEventDataMacOs extends RawKeyEventData { ...@@ -154,13 +154,15 @@ class RawKeyEventDataMacOs extends RawKeyEventData {
return _isLeftRightModifierPressed(side, independentModifier & modifierCommand, modifierLeftCommand, modifierRightCommand); return _isLeftRightModifierPressed(side, independentModifier & modifierCommand, modifierLeftCommand, modifierRightCommand);
case ModifierKey.capsLockModifier: case ModifierKey.capsLockModifier:
return independentModifier & modifierCapsLock != 0; return independentModifier & modifierCapsLock != 0;
case ModifierKey.numLockModifier: // On macOS, the function modifier bit is set for any function key, like F1,
return independentModifier & modifierNumericPad != 0; // F2, etc., but the meaning of ModifierKey.modifierFunction in Flutter is
// that of the Fn modifier key, so there's no good way to emulate that on
// macOS.
case ModifierKey.functionModifier: case ModifierKey.functionModifier:
return independentModifier & modifierFunction != 0; case ModifierKey.numLockModifier:
case ModifierKey.symbolModifier: case ModifierKey.symbolModifier:
case ModifierKey.scrollLockModifier: case ModifierKey.scrollLockModifier:
// These are not used in macOS keyboards. // These modifier masks are not used in macOS keyboards.
return false; return false;
} }
return false; return false;
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -751,7 +752,7 @@ void main() { ...@@ -751,7 +752,7 @@ void main() {
} }
} }
// Test to make sure that we follow the same path backwards and forwards. // Test to make sure that the same path is followed backwards and forwards.
await tester.pump(); await tester.pump();
expectState(<bool>[null, null, null, null, true, null]); expectState(<bool>[null, null, null, null, true, null]);
clear(); clear();
...@@ -1004,7 +1005,7 @@ void main() { ...@@ -1004,7 +1005,7 @@ void main() {
expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue); expect(Focus.of(lowerLeftKey.currentContext).hasPrimaryFocus, isTrue);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue); expect(Focus.of(upperLeftKey.currentContext).hasPrimaryFocus, isTrue);
}); }, skip: kIsWeb);
testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async { testWidgets('Arrow focus traversal actions can be re-enabled for text fields.', (WidgetTester tester) async {
final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey'); final GlobalKey upperLeftKey = GlobalKey(debugLabel: 'upperLeftKey');
final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey'); final GlobalKey upperRightKey = GlobalKey(debugLabel: 'upperRightKey');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment