// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { group(LogicalKeySet, () { test('LogicalKeySet passes parameters correctly.', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final LogicalKeySet set2 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ); final LogicalKeySet set3 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, ); final LogicalKeySet set4 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final LogicalKeySet setFromSet = LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }); expect( set1.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, }), ); expect( set2.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, }), ); expect( set3.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, }), ); expect( set4.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); expect( setFromSet.keys, equals(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, }), ); }); test('LogicalKeySet works as a map key.', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); final LogicalKeySet set2 = LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, ); final LogicalKeySet set3 = LogicalKeySet( LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, ); final LogicalKeySet set4 = LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA, }); final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'}; expect(set2 == set3, isTrue); expect(set2 == set4, isTrue); expect(set2.hashCode, set3.hashCode); expect(set2.hashCode, set4.hashCode); expect(map.containsKey(set1), isTrue); expect(map.containsKey(LogicalKeySet(LogicalKeyboardKey.keyA)), isTrue); expect( set2, equals(LogicalKeySet.fromSet(<LogicalKeyboardKey>{ LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD, })), ); }); testWidgets('handles two keys', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( LogicalKeySet( LogicalKeyboardKey.keyC, LogicalKeyboardKey.control, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // KeyC -> LCtrl: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // LCtrl -> LShift -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); invoked = 0; // LCtrl -> KeyA -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }); test('LogicalKeySet.hashCode is stable', () { final LogicalKeySet set1 = LogicalKeySet(LogicalKeyboardKey.keyA); expect(set1.hashCode, set1.hashCode); final LogicalKeySet set2 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB); expect(set2.hashCode, set2.hashCode); final LogicalKeySet set3 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC); expect(set3.hashCode, set3.hashCode); final LogicalKeySet set4 = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD); expect(set4.hashCode, set4.hashCode); }); test('LogicalKeySet.hashCode is order-independent', () { expect( LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, LogicalKeySet(LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB).hashCode, LogicalKeySet(LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC).hashCode, LogicalKeySet(LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); expect( LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyD).hashCode, LogicalKeySet(LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyA).hashCode, ); }); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( Focus( autofocus: true, onKey: (FocusNode node, RawKeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); final LogicalKeySet set = LogicalKeySet(LogicalKeyboardKey.keyA, LogicalKeyboardKey.control); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(set, events[0]), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(ShortcutActivator.isActivatedBy(set, events[0]), isFalse); }); test('LogicalKeySet diagnostics work.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals('keys: Key A + Key B')); }); }); group(SingleActivator, () { testWidgets('handles Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // KeyC -> LCtrl: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // LShift -> LCtrl -> KeyC: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); invoked = 0; // With Ctrl-C pressed, KeyA -> Release KeyA: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); invoked = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // LCtrl -> KeyA -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); invoked = 0; // RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // LCtrl -> RCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; // While holding Ctrl-C, press KeyA: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC); expect(invoked, 2); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 2); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('rejects repeated events if requested', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, control: true, includeRepeats: false, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles Shift-Ctrl-C', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const SingleActivator( LogicalKeyboardKey.keyC, shift: true, control: true, ), (Intent intent) { invoked += 1; }, )); await tester.pump(); // LShift -> LCtrl -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; // LCtrl -> LShift -> KeyC: Accept await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; // LCtrl -> KeyC -> LShift: Reject await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 0); invoked = 0; expect(RawKeyboard.instance.keysPressed, isEmpty); }); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( Focus( autofocus: true, onKey: (FocusNode node, RawKeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const SingleActivator singleActivator = SingleActivator(LogicalKeyboardKey.keyA, control: true); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isTrue); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(ShortcutActivator.isActivatedBy(singleActivator, events[1]), isFalse); }); group('diagnostics.', () { test('single key', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals('keys: Key A')); }); test('no repeats', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, includeRepeats: false, ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(2)); expect(description[0], equals('keys: Key A')); expect(description[1], equals('excluding repeats')); }); test('combination', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const SingleActivator( LogicalKeyboardKey.keyA, control: true, shift: true, alt: true, meta: true, ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals('keys: Control + Alt + Meta + Shift + Key A')); }); }); }); group(Shortcuts, () { testWidgets('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async { const Shortcuts shortcuts = Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); testWidgets('Default constructed Shortcuts.manager has empty shortcuts', (WidgetTester tester) async { final ShortcutManager manager = ShortcutManager(); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, isEmpty); final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isEmpty); }); testWidgets('Shortcuts.manager passes on shortcuts', (WidgetTester tester) async { final Map<LogicalKeySet, Intent> testShortcuts = <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }; final ShortcutManager manager = ShortcutManager(shortcuts: testShortcuts); expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, equals(testShortcuts)); final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox()); await tester.pumpWidget(shortcuts); expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, equals(testShortcuts)); }); testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); testWidgets('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); testWidgets('ShortcutManager ignores key presses with no primary focus', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return true; }, ), }, child: Shortcuts.manager( manager: testManager, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ); await tester.pump(); expect(primaryFocus, isNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isFalse); expect(pressedKeys, isEmpty); }); testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( Shortcuts.manager( manager: testManager, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); }); testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Shortcuts.manager( manager: testManager, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing, }, child: Focus( autofocus: true, child: SizedBox(key: containerKey, width: 100, height: 100), ), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, isFalse); expect(pressedKeys, isEmpty); }); testWidgets("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ); await tester.pump(); final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(handled, isFalse); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); }); testWidgets('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ); await tester.pump(); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isTrue); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); expect(invoked, isTrue); }); testWidgets('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: DoNothingAction(consumesKey: false), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isFalse); expect(invoked, isFalse); }); testWidgets('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async { final GlobalKey textFieldKey = GlobalKey(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), }, ); bool invoked = false; await tester.pumpWidget( MaterialApp( home: Material( child: Shortcuts.manager( manager: testManager, child: Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invoked = true; return invoked; }, ), }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingAndStopPropagationIntent(), }, child: TextField(key: textFieldKey, autofocus: true), ), ), ), ), ), ); await tester.pump(); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); expect(result, isFalse); expect(invoked, isFalse); }); test('Shortcuts diagnostics work.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.shift, LogicalKeyboardKey.keyA, ): const ActivateIntent(), LogicalKeySet( LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight, ): const DirectionalFocusIntent(TraversalDirection.right), }, child: const SizedBox(), ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect( description[0], equalsIgnoringHashCodes( 'shortcuts: {{Shift + Key A}: ActivateIntent#00000, {Shift + Arrow Right}: DirectionalFocusIntent#00000(direction: right)}', ), ); }); test('Shortcuts diagnostics work when debugLabel specified.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( debugLabel: '<Debug Label>', shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ): const ActivateIntent(), }, child: const SizedBox(), ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals('shortcuts: <Debug Label>')); }); test('Shortcuts diagnostics work when manager not specified.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ): const ActivateIntent(), }, child: const SizedBox(), ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}')); }); test('Shortcuts diagnostics work when manager specified.', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final TestShortcutManager testManager = TestShortcutManager( pressedKeys, shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet( LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyB, ): const ActivateIntent(), }, ); Shortcuts.manager( manager: testManager, child: const SizedBox(), ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(2)); expect(description[0], equalsIgnoringHashCodes('manager: TestShortcutManager#00000(shortcuts: {LogicalKeySet#00000(keys: Key A + Key B): ActivateIntent#00000})')); expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}')); }); testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async { tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; bool? value = true; Widget buildApp() { return MaterialApp( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.space): const PrioritizedIntents( orderedIntents: <Intent>[ ActivateIntent(), ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page), ], ), LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(), LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page), }, home: Material( child: Center( child: ListView( primary: true, children: <Widget> [ StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Checkbox( value: value, onChanged: (bool? newValue) => setState(() { value = newValue; }), focusColor: Colors.orange[500], ); }, ), Container( color: Colors.blue, height: 1000, ), ], ), ), ), ); } await tester.pumpWidget(buildApp()); await tester.pumpAndSettle(); expect( tester.binding.focusManager.primaryFocus!.toStringShort(), equalsIgnoringHashCodes('FocusScopeNode#00000(_ModalScopeState<dynamic> Focus Scope [PRIMARY FOCUS])'), ); final ScrollController controller = PrimaryScrollController.of( tester.element(find.byType(ListView)), ); expect(controller.position.pixels, 0.0); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); // ScrollView scrolls expect(controller.position.pixels, 448.0); expect(value, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.pageUp); await tester.pumpAndSettle(); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pumpAndSettle(); // Focus is now on the checkbox. expect( tester.binding.focusManager.primaryFocus!.toStringShort(), equalsIgnoringHashCodes('FocusNode#00000([PRIMARY FOCUS])'), ); expect(value, isTrue); expect(controller.position.pixels, 0.0); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); // Checkbox is toggled, scroll view does not scroll. expect(value, isFalse); expect(controller.position.pixels, 0.0); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(value, isTrue); expect(controller.position.pixels, 0.0); }); testWidgets('Shortcuts support activators that returns null in triggers', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const DumbLogicalActivator(LogicalKeyboardKey.keyC), (Intent intent) { invoked += 1; }, const SingleActivator(LogicalKeyboardKey.keyC, control: true), (Intent intent) { invoked += 10; }, )); await tester.pump(); // Press KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); invoked = 0; // Press ControlLeft + KeyC: Accepted by SingleActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 10); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 10); invoked = 0; // Press ControlLeft + ShiftLeft + KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; }); }); group('CharacterActivator', () { testWidgets('is triggered on events with correct character', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles repeated events', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?'), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press KeyC: Accepted by DumbLogicalActivator await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 2); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 2); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('rejects repeated events if requested', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?', includeRepeats: false), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyRepeatEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); expect(invoked, 1); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('handles Alt, Ctrl and Meta', (WidgetTester tester) async { int invoked = 0; await tester.pumpWidget(activatorTester( const CharacterActivator('?', alt: true, meta: true, control: true), (Intent intent) { invoked += 1; }, )); await tester.pump(); // Press Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); expect(invoked, 0); // Press Left Alt + Ctrl + Meta + Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.altLeft); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); expect(invoked, 1); invoked = 0; // Press Right Alt + Ctrl + Meta + Shift + / await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.altRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.metaRight); await tester.sendKeyDownEvent(LogicalKeyboardKey.controlRight); expect(invoked, 0); await tester.sendKeyDownEvent(LogicalKeyboardKey.slash, character: '?'); expect(invoked, 1); await tester.sendKeyUpEvent(LogicalKeyboardKey.slash); await tester.sendKeyUpEvent(LogicalKeyboardKey.shiftRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.metaRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.altRight); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlRight); expect(invoked, 1); invoked = 0; }, variant: KeySimulatorTransitModeVariant.all()); testWidgets('isActivatedBy works as expected', (WidgetTester tester) async { // Collect some key events to use for testing. final List<RawKeyEvent> events = <RawKeyEvent>[]; await tester.pumpWidget( Focus( autofocus: true, onKey: (FocusNode node, RawKeyEvent event) { events.add(event); return KeyEventResult.ignored; }, child: const SizedBox(), ), ); const CharacterActivator characterActivator = CharacterActivator('a'); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(ShortcutActivator.isActivatedBy(characterActivator, events[0]), isTrue); }); group('diagnostics.', () { test('single key', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A').debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals("character: 'A'")); }); test('no repeats', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A', includeRepeats: false) .debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(2)); expect(description[0], equals("character: 'A'")); expect(description[1], equals('excluding repeats')); }); test('combination', () { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); const CharacterActivator('A', control: true, meta: true, ).debugFillProperties(builder); final List<String> description = builder.properties.where((DiagnosticsNode node) { return !node.isFiltered(DiagnosticLevel.info); }).map((DiagnosticsNode node) => node.toString()).toList(); expect(description.length, equals(1)); expect(description[0], equals("character: Control + Meta + 'A'")); }); }); }); group('CallbackShortcuts', () { testWidgets('trigger on key events', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invokedA += 1; }, const SingleActivator(LogicalKeyboardKey.keyB): () { invokedB += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets('nested CallbackShortcuts stop propagation', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const SingleActivator(LogicalKeyboardKey.keyA): () { invokedInner += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); }); testWidgets('non-overlapping nested CallbackShortcuts fire appropriately', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('b'): () { invokedOuter += 1; }, }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('a'): () { invokedInner += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); }); testWidgets('Works correctly with Shortcuts too', (WidgetTester tester) async { int invokedCallbackA = 0; int invokedCallbackB = 0; int invokedActionA = 0; int invokedActionB = 0; void clear() { invokedCallbackA = 0; invokedCallbackB = 0; invokedActionA = 0; invokedActionB = 0; } await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invokedActionA += 1; return true; }, ), TestIntent2: TestAction( onInvoke: (Intent intent) { invokedActionB += 1; return true; }, ), }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('b'): () { invokedCallbackB += 1; }, }, child: Shortcuts( shortcuts: <LogicalKeySet, Intent>{ LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(), LogicalKeySet(LogicalKeyboardKey.keyB): const TestIntent2(), }, child: CallbackShortcuts( bindings: <ShortcutActivator, VoidCallback>{ const CharacterActivator('a'): () { invokedCallbackA += 1; }, }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedCallbackA, equals(1)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); clear(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedCallbackA, equals(0)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); }); }); group('ShortcutRegistrar', () { testWidgets('trigger ShortcutRegistrar on key events', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedA += 1; }), const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { invokedB += 1; }), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets('MaterialApp has a ShortcutRegistrar listening', (WidgetTester tester) async { int invokedA = 0; int invokedB = 0; await tester.pumpWidget( MaterialApp( home: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedA += 1; }), const SingleActivator(LogicalKeyboardKey.keyB): VoidCallbackIntent(() { invokedB += 1; }), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); await tester.pump(); expect(invokedA, equals(1)); expect(invokedB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedA, equals(1)); expect(invokedB, equals(0)); invokedA = 0; invokedB = 0; await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedA, equals(0)); expect(invokedB, equals(1)); }); testWidgets("doesn't override text field shortcuts", (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); await tester.pumpWidget( MaterialApp( home: Scaffold( body: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA, control: true): SelectAllTextIntent(SelectionChangedCause.keyboard), }, child: TextField( autofocus: true, controller: controller, ), ), ), ), ), ); controller.text = 'Testing'; await tester.pump(); // Send a "Ctrl-A", which should be bound to select all by default. await tester.sendKeyDownEvent(LogicalKeyboardKey.controlLeft); await tester.sendKeyEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.controlLeft); await tester.pump(); expect(controller.selection.baseOffset, equals(0)); expect(controller.selection.extentOffset, equals(7)); }); testWidgets('nested ShortcutRegistrars stop propagation', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedOuter += 1; }), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const SingleActivator(LogicalKeyboardKey.keyA): VoidCallbackIntent(() { invokedInner += 1; }), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ),), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); }); testWidgets('non-overlapping nested ShortcutRegistrars fire appropriately', (WidgetTester tester) async { int invokedOuter = 0; int invokedInner = 0; await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const CharacterActivator('b'): VoidCallbackIntent(() { invokedOuter += 1; }), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const CharacterActivator('a'): VoidCallbackIntent(() { invokedInner += 1; }), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedOuter, equals(0)); expect(invokedInner, equals(1)); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); expect(invokedOuter, equals(1)); expect(invokedInner, equals(1)); }); testWidgets('Works correctly with Shortcuts too', (WidgetTester tester) async { int invokedCallbackA = 0; int invokedCallbackB = 0; int invokedActionA = 0; int invokedActionB = 0; void clear() { invokedCallbackA = 0; invokedCallbackB = 0; invokedActionA = 0; invokedActionB = 0; } await tester.pumpWidget( Actions( actions: <Type, Action<Intent>>{ TestIntent: TestAction( onInvoke: (Intent intent) { invokedActionA += 1; return true; }, ), TestIntent2: TestAction( onInvoke: (Intent intent) { invokedActionB += 1; return true; }, ), VoidCallbackIntent: VoidCallbackAction(), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const CharacterActivator('b'): VoidCallbackIntent(() { invokedCallbackB += 1; }), }, child: Shortcuts( shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): TestIntent(), SingleActivator(LogicalKeyboardKey.keyB): TestIntent2(), }, child: ShortcutRegistrar( child: TestCallbackRegistration( shortcuts: <ShortcutActivator, Intent>{ const CharacterActivator('a'): VoidCallbackIntent(() { invokedCallbackA += 1; }), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ), ), ), ); await tester.pump(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); expect(invokedCallbackA, equals(1)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(0)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); clear(); await tester.sendKeyDownEvent(LogicalKeyboardKey.keyB); expect(invokedCallbackA, equals(0)); expect(invokedCallbackB, equals(0)); expect(invokedActionA, equals(0)); expect(invokedActionB, equals(1)); await tester.sendKeyUpEvent(LogicalKeyboardKey.keyB); }); testWidgets('Updating shortcuts triggers dependency rebuild', (WidgetTester tester) async { final List<Map<ShortcutActivator, Intent>> shortcutsChanged = <Map<ShortcutActivator, Intent>>[]; void dependenciesUpdated(Map<ShortcutActivator, Intent> shortcuts) { shortcutsChanged.add(shortcuts); } await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ); await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ); await tester.pumpWidget( ShortcutRegistrar( child: TestCallbackRegistration( onDependencyUpdate: dependenciesUpdated, shortcuts: const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), }, child: Actions( actions: <Type, Action<Intent>>{ VoidCallbackIntent: VoidCallbackAction(), }, child: const Focus( autofocus: true, child: Placeholder(), ), ), ), ), ); expect(shortcutsChanged.length, equals(2)); expect(shortcutsChanged.last, equals(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): SelectAllTextIntent(SelectionChangedCause.keyboard), SingleActivator(LogicalKeyboardKey.keyB): ActivateIntent(), })); }); testWidgets('using a disposed token asserts', (WidgetTester tester) async { final ShortcutRegistry registry = ShortcutRegistry(); final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); token.dispose(); expect(() {token.replaceAll(<ShortcutActivator, Intent>{}); }, throwsFlutterError); }); testWidgets('setting duplicate bindings asserts', (WidgetTester tester) async { final ShortcutRegistry registry = ShortcutRegistry(); final ShortcutRegistryEntry token = registry.addAll(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): DoNothingIntent(), }); expect(() { final ShortcutRegistryEntry token2 = registry.addAll(const <ShortcutActivator, Intent>{ SingleActivator(LogicalKeyboardKey.keyA): ActivateIntent(), }); token2.dispose(); }, throwsAssertionError); token.dispose(); }); }); } class TestCallbackRegistration extends StatefulWidget { const TestCallbackRegistration({super.key, required this.shortcuts, this.onDependencyUpdate, required this.child}); final Map<ShortcutActivator, Intent> shortcuts; final void Function(Map<ShortcutActivator, Intent> shortcuts)? onDependencyUpdate; final Widget child; @override State<TestCallbackRegistration> createState() => _TestCallbackRegistrationState(); } class _TestCallbackRegistrationState extends State<TestCallbackRegistration> { ShortcutRegistryEntry? _registryToken; @override void didChangeDependencies() { super.didChangeDependencies(); _registryToken?.dispose(); _registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts); } @override void didUpdateWidget(TestCallbackRegistration oldWidget) { super.didUpdateWidget(oldWidget); if (widget.shortcuts != oldWidget.shortcuts || _registryToken == null) { _registryToken?.dispose(); _registryToken = ShortcutRegistry.of(context).addAll(widget.shortcuts); } widget.onDependencyUpdate?.call(ShortcutRegistry.of(context).shortcuts); } @override void dispose() { _registryToken?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return widget.child; } } class TestAction extends CallbackAction<Intent> { TestAction({ required super.onInvoke, }); } /// An activator that accepts down events that has [key] as the logical key. /// /// This class is used only to tests. It is intentionally designed poorly by /// returning null in [triggers], and checks [key] in [accepts]. class DumbLogicalActivator extends ShortcutActivator { const DumbLogicalActivator(this.key); final LogicalKeyboardKey key; @override Iterable<LogicalKeyboardKey>? get triggers => null; @override bool accepts(RawKeyEvent event, RawKeyboard state) { return event is RawKeyDownEvent && event.logicalKey == key; } /// Returns a short and readable description of the key combination. /// /// Intended to be used in debug mode for logging purposes. In release mode, /// [debugDescribeKeys] returns an empty string. @override String debugDescribeKeys() { String result = ''; assert(() { result = key.keyLabel; return true; }()); return result; } } class TestIntent extends Intent { const TestIntent(); } class TestIntent2 extends Intent { const TestIntent2(); } class TestShortcutManager extends ShortcutManager { TestShortcutManager(this.keys, { super.shortcuts }); List<LogicalKeyboardKey> keys; @override KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) { if (event is RawKeyDownEvent) { keys.add(event.logicalKey); } return super.handleKeypress(context, event); } } Widget activatorTester( ShortcutActivator activator, ValueSetter<Intent> onInvoke, [ ShortcutActivator? activator2, ValueSetter<Intent>? onInvoke2, ]) { final bool hasSecond = activator2 != null && onInvoke2 != null; return Actions( key: GlobalKey(), actions: <Type, Action<Intent>>{ TestIntent: TestAction(onInvoke: (Intent intent) { onInvoke(intent); return true; }), if (hasSecond) TestIntent2: TestAction(onInvoke: (Intent intent) { onInvoke2(intent); return null; }), }, child: Shortcuts( shortcuts: <ShortcutActivator, Intent>{ activator: const TestIntent(), if (hasSecond) activator2: const TestIntent2(), }, child: const Focus( autofocus: true, child: SizedBox(width: 100, height: 100), ), ), ); }