Activate shortcuts based on NumLock state (#145146)

## Description

The PR updates `SingleActivator` in order to add a parameter for specifying that a shortcut depends on <kbd>NumLock</kbd> key state. 

Somewhat similarly to what is possible with common modifiers expect that a boolean is not enough in this case because: by default, a shortcut should ignore the <kbd>NumLock</kbd> state and it should be possible to define shortcuts that require <kbd>NumLock</kbd> to be locked and other that require it to be unlocked.

@gspencergoog I considered defining a new `ShortcutActivator` implementation for this, but I thinks that adding the feature directly to `SingleActivator` results in a cleaner API.

## Related Issue

Fixes https://github.com/flutter/flutter/issues/145144
Preparation for https://github.com/flutter/flutter/issues/144936

## Tests

Adds 3 tests.
This commit is contained in:
Bruno Leroux 2024-03-19 09:27:50 +01:00 committed by GitHub
parent 88a9b58dd8
commit 6f61f6135f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 140 additions and 2 deletions

View file

@ -142,6 +142,16 @@ class KeySet<T extends KeyboardKey> {
}
}
/// Determines how the state of a lock key is used to accept a shortcut.
enum LockState {
/// The lock key state is not used to determine [SingleActivator.accepts] result.
ignored,
/// The lock key must be locked to trigger the shortcut.
locked,
/// The lock key must be unlocked to trigger the shortcut.
unlocked,
}
/// An interface to define the keyboard key combination to trigger a shortcut.
///
/// [ShortcutActivator]s are used by [Shortcuts] widgets, and are mapped to
@ -430,6 +440,7 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
this.shift = false,
this.alt = false,
this.meta = false,
this.numLock = LockState.ignored,
this.includeRepeats = true,
}) : // The enumerated check with `identical` is cumbersome but the only way
// since const constructors can not call functions such as `==` or
@ -505,6 +516,19 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
/// * [LogicalKeyboardKey.metaLeft], [LogicalKeyboardKey.metaRight].
final bool meta;
/// Whether the NumLock key state should be checked for [trigger] to activate
/// the shortcut.
///
/// It defaults to [LockState.ignored], meaning the NumLock state is ignored
/// when the event is received in order to activate the shortcut.
/// If it's [LockState.locked], then the NumLock key must be locked.
/// If it's [LockState.unlocked], then the NumLock key must be unlocked.
///
/// See also:
///
/// * [LogicalKeyboardKey.numLock].
final LockState numLock;
/// Whether this activator accepts repeat events of the [trigger] key.
///
/// If [includeRepeats] is true, the activator is checked on all
@ -525,11 +549,20 @@ class SingleActivator with Diagnosticable, MenuSerializableShortcut implements S
&& meta == pressed.intersection(_metaSynonyms).isNotEmpty;
}
bool _shouldAcceptNumLock(HardwareKeyboard state) {
return switch (numLock) {
LockState.ignored => true,
LockState.locked => state.lockModesEnabled.contains(KeyboardLockMode.numLock),
LockState.unlocked => !state.lockModesEnabled.contains(KeyboardLockMode.numLock),
};
}
@override
bool accepts(KeyEvent event, HardwareKeyboard state) {
return (event is KeyDownEvent || (includeRepeats && event is KeyRepeatEvent))
&& triggers.contains(event.logicalKey)
&& _shouldAcceptModifiers(state.logicalKeysPressed);
&& _shouldAcceptModifiers(state.logicalKeysPressed)
&& _shouldAcceptNumLock(state);
}
@override

View file

@ -474,7 +474,7 @@ void main() {
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft);
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyA);
@ -497,6 +497,111 @@ void main() {
expect(ShortcutActivator.isActivatedBy(noRepeatSingleActivator, events.last), isFalse);
});
testWidgets('numLock works as expected when set to LockState.locked', (WidgetTester tester) async {
// Collect some key events to use for testing.
final List<KeyEvent> events = <KeyEvent>[];
await tester.pumpWidget(
Focus(
autofocus: true,
onKeyEvent: (FocusNode node, KeyEvent event) {
events.add(event);
return KeyEventResult.ignored;
},
child: const SizedBox(),
),
);
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.numpad4, numLock: LockState.locked);
// Lock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
// Unlock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
});
testWidgets('numLock works as expected when set to LockState.unlocked', (WidgetTester tester) async {
// Collect some key events to use for testing.
final List<KeyEvent> events = <KeyEvent>[];
await tester.pumpWidget(
Focus(
autofocus: true,
onKeyEvent: (FocusNode node, KeyEvent event) {
events.add(event);
return KeyEventResult.ignored;
},
child: const SizedBox(),
),
);
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.numpad4, numLock: LockState.unlocked);
// Lock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isFalse);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
// Unlock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
});
testWidgets('numLock works as expected when set to LockState.ignored', (WidgetTester tester) async {
// Collect some key events to use for testing.
final List<KeyEvent> events = <KeyEvent>[];
await tester.pumpWidget(
Focus(
autofocus: true,
onKeyEvent: (FocusNode node, KeyEvent event) {
events.add(event);
return KeyEventResult.ignored;
},
child: const SizedBox(),
),
);
const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.numpad4);
// Lock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
// Unlock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse);
await tester.sendKeyDownEvent(LogicalKeyboardKey.numpad4);
expect(ShortcutActivator.isActivatedBy(singleActivator, events.last), isTrue);
await tester.sendKeyUpEvent(LogicalKeyboardKey.numpad4);
});
group('diagnostics.', () {
test('single key', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();