Adds numpad navigation shortcuts for Linux (#145464)

## Description

This PR adds shortcuts related to numpad keys on Linux.

## Related Issue

Linux side for https://github.com/flutter/flutter/issues/144936

## Tests

Adds 2 tests.
This commit is contained in:
Bruno Leroux 2024-03-22 07:22:09 +01:00 committed by GitHub
parent 5fab92f062
commit 859eb2eda9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 224 additions and 0 deletions

View file

@ -253,8 +253,51 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
static final Map<ShortcutActivator, Intent> _fuchsiaShortcuts = _androidShortcuts;
static final Map<ShortcutActivator, Intent> _linuxNumpadShortcuts = <ShortcutActivator, Intent>{
// When numLock is on, numpad keys shortcuts require shift to be pressed too.
const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, numLock: LockState.locked): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad4, shift: true, numLock: LockState.locked): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad8, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad2, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad4, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad8, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad2, shift: true, control: true, numLock: LockState.locked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad9, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad3, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad7, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpad1, shift: true, numLock: LockState.locked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true, numLock: LockState.locked): const DeleteCharacterIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true, control: true, numLock: LockState.locked): const DeleteToNextWordBoundaryIntent(forward: true),
// When numLock is off, numpad keys shortcuts require shift not to be pressed.
const SingleActivator(LogicalKeyboardKey.numpad6, numLock: LockState.unlocked): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad4, numLock: LockState.unlocked): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad8, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad2, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad6, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad4, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad8, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad2, control: true, numLock: LockState.unlocked): const ExtendSelectionToNextParagraphBoundaryIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad9, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad3, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad7, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpad1, numLock: LockState.unlocked): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.numpadDecimal, numLock: LockState.unlocked): const DeleteCharacterIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.numpadDecimal, control: true, numLock: LockState.unlocked): const DeleteToNextWordBoundaryIntent(forward: true),
};
static final Map<ShortcutActivator, Intent> _linuxShortcuts = <ShortcutActivator, Intent>{
..._commonShortcuts,
..._linuxNumpadShortcuts,
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),

View file

@ -486,6 +486,185 @@ void main() {
expect(state.lastIntent, isA<ExtendSelectionToLineBreakIntent>());
}, variant: macOSOnly);
}, skip: kIsWeb); // [intended] specific tests target non-web.
group('Linux does accept numpad shortcuts', () {
testWidgets('when numlock is locked', (WidgetTester tester) async {
final FocusNode editable = FocusNode();
addTearDown(editable.dispose);
final FocusNode spy = FocusNode();
addTearDown(spy.dispose);
await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
spy.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));
// Lock NumLock.
await tester.sendKeyEvent(LogicalKeyboardKey.numLock);
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isTrue);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6, shift: true));
expect(state.lastIntent, isA<ExtendSelectionByCharacterIntent>());
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4, shift: true));
expect(state.lastIntent, isA<ExtendSelectionByCharacterIntent>());
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad8, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad2, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad9, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentPageIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad3, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentPageIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad7, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad1, shift: true));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true));
expect(state.lastIntent, isA<DeleteCharacterIntent>());
expect((state.lastIntent! as DeleteCharacterIntent).forward, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4, shift: true, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad8, shift: true, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextParagraphBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad2, shift: true, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextParagraphBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).collapseSelection, false);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpadDecimal, shift: true, control: true));
expect(state.lastIntent, isA<DeleteToNextWordBoundaryIntent>());
expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true);
}, variant: TargetPlatformVariant.only(TargetPlatform.linux));
testWidgets('when numlock is unlocked', (WidgetTester tester) async {
final FocusNode editable = FocusNode();
addTearDown(editable.dispose);
final FocusNode spy = FocusNode();
addTearDown(spy.dispose);
await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
spy.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));
// Verify that NumLock is unlocked.
expect(HardwareKeyboard.instance.lockModesEnabled.contains(KeyboardLockMode.numLock), isFalse);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6));
expect(state.lastIntent, isA<ExtendSelectionByCharacterIntent>());
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4));
expect(state.lastIntent, isA<ExtendSelectionByCharacterIntent>());
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionByCharacterIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad8));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad2));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad9));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentPageIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad3));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentPageIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentPageIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad7));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad1));
expect(state.lastIntent, isA<ExtendSelectionVerticallyToAdjacentLineIntent>());
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionVerticallyToAdjacentLineIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpadDecimal));
expect(state.lastIntent, isA<DeleteCharacterIntent>());
expect((state.lastIntent! as DeleteCharacterIntent).forward, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad4, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextWordBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionToNextWordBoundaryIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad8, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextParagraphBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).forward, false);
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad2, control: true));
expect(state.lastIntent, isA<ExtendSelectionToNextParagraphBoundaryIntent>());
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).forward, true);
expect((state.lastIntent! as ExtendSelectionToNextParagraphBoundaryIntent).collapseSelection, true);
await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpadDecimal, control: true));
expect(state.lastIntent, isA<DeleteToNextWordBoundaryIntent>());
expect((state.lastIntent! as DeleteToNextWordBoundaryIntent).forward, true);
}, variant: TargetPlatformVariant.only(TargetPlatform.linux));
}, skip: kIsWeb); // [intended] specific tests target non-web.
}
class ActionSpy extends StatefulWidget {
@ -508,6 +687,8 @@ class ActionSpyState extends State<ActionSpy> {
ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent),
ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent),
ExtendSelectionToNextParagraphBoundaryIntent: CallbackAction<ExtendSelectionToNextParagraphBoundaryIntent>(onInvoke: _captureIntent),
ExtendSelectionVerticallyToAdjacentPageIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentPageIntent>(onInvoke: _captureIntent),
DeleteToLineBreakIntent: CallbackAction<DeleteToLineBreakIntent>(onInvoke: _captureIntent),
DeleteToNextWordBoundaryIntent: CallbackAction<DeleteToNextWordBoundaryIntent>(onInvoke: _captureIntent),