diff --git a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart index 8b350418814..46ec7c22594 100644 --- a/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart +++ b/packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart @@ -253,8 +253,51 @@ class DefaultTextEditingShortcuts extends StatelessWidget { static final Map _fuchsiaShortcuts = _androidShortcuts; + static final Map _linuxNumpadShortcuts = { + // 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 _linuxShortcuts = { ..._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), diff --git a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart index a68002c933d..96b4c9c5353 100644 --- a/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart +++ b/packages/flutter/test/widgets/default_text_editing_shortcuts_test.dart @@ -486,6 +486,185 @@ void main() { expect(state.lastIntent, isA()); }, 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(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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + expect((state.lastIntent! as DeleteCharacterIntent).forward, true); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6, shift: true, control: true)); + expect(state.lastIntent, isA()); + 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()); + 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()); + 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()); + 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()); + 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(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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + expect((state.lastIntent! as DeleteCharacterIntent).forward, true); + + await sendKeyCombination(tester, const SingleActivator(LogicalKeyboardKey.numpad6, control: true)); + expect(state.lastIntent, isA()); + 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()); + 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()); + 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()); + 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()); + 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 { ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction(onInvoke: _captureIntent), ExtendSelectionToDocumentBoundaryIntent: CallbackAction(onInvoke: _captureIntent), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction(onInvoke: _captureIntent), + ExtendSelectionToNextParagraphBoundaryIntent: CallbackAction(onInvoke: _captureIntent), + ExtendSelectionVerticallyToAdjacentPageIntent: CallbackAction(onInvoke: _captureIntent), DeleteToLineBreakIntent: CallbackAction(onInvoke: _captureIntent), DeleteToNextWordBoundaryIntent: CallbackAction(onInvoke: _captureIntent),