Unverified Commit da24f105 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Removing Shorcuts.of and Shortctus.maybeOf (#104215)

This removes Shorcuts.of and Shortctus.maybeOf because they're not especially useful, since the only thing you can really set on a ShortcutManager is the shortcuts, and the Shortcuts widget that you give it to manages those, so if it rebuilds, it overwrites what you set.

Also, adds a Shortcuts.manager constructor and removes the manager argument to the Shortcuts widget.

Removing these will also eliminate an InheritedWidget for each Shortcuts widget, improving memory usage.
parent d5fbc375
...@@ -97,6 +97,10 @@ typedef ActionListenerCallback = void Function(Action<Intent> action); ...@@ -97,6 +97,10 @@ typedef ActionListenerCallback = void Function(Action<Intent> action);
/// developers to change that if they add an ancestor [Actions] widget that maps /// developers to change that if they add an ancestor [Actions] widget that maps
/// [SelectAllTextIntent] to a different [Action]. /// [SelectAllTextIntent] to a different [Action].
/// ///
/// See the article on [Using Actions and
/// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts)
/// for a detailed explanation.
///
/// See also: /// See also:
/// ///
/// * [Shortcuts], which is a widget that contains a key map, in which it looks /// * [Shortcuts], which is a widget that contains a key map, in which it looks
......
...@@ -636,13 +636,15 @@ class _ActivatorIntentPair with Diagnosticable { ...@@ -636,13 +636,15 @@ class _ActivatorIntentPair with Diagnosticable {
} }
} }
/// A manager of keyboard shortcut bindings. /// A manager of keyboard shortcut bindings used by [Shortcuts] to handle key
/// /// events.
/// A `ShortcutManager` is obtained by calling [Shortcuts.of] on the context of
/// the widget that you want to find a manager for.
/// ///
/// The manager may be listened to (with [addListener]/[removeListener]) for /// The manager may be listened to (with [addListener]/[removeListener]) for
/// change notifications when the shortcuts change. /// change notifications when the shortcuts change.
///
/// Typically, a [Shortcuts] widget supplies its own manager, but in uncommon
/// cases where overriding the usual shortcut manager behavior is desired, a
/// subclassed [ShortcutManager] may be supplied.
class ShortcutManager with Diagnosticable, ChangeNotifier { class ShortcutManager with Diagnosticable, ChangeNotifier {
/// Constructs a [ShortcutManager]. /// Constructs a [ShortcutManager].
ShortcutManager({ ShortcutManager({
...@@ -773,6 +775,10 @@ class ShortcutManager with Diagnosticable, ChangeNotifier { ...@@ -773,6 +775,10 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
/// when invoking an [Action] via a keyboard key combination that maps to an /// when invoking an [Action] via a keyboard key combination that maps to an
/// [Intent]. /// [Intent].
/// ///
/// See the article on [Using Actions and
/// Shortcuts](https://docs.flutter.dev/development/ui/advanced/actions_and_shortcuts)
/// for a detailed explanation.
///
/// {@tool dartpad} /// {@tool dartpad}
/// Here, we will use the [Shortcuts] and [Actions] widgets to add and subtract /// Here, we will use the [Shortcuts] and [Actions] widgets to add and subtract
/// from a counter. When the child widget has keyboard focus, and a user presses /// from a counter. When the child widget has keyboard focus, and a user presses
...@@ -810,35 +816,61 @@ class ShortcutManager with Diagnosticable, ChangeNotifier { ...@@ -810,35 +816,61 @@ class ShortcutManager with Diagnosticable, ChangeNotifier {
/// * [Action], a class for defining an invocation of a user action. /// * [Action], a class for defining an invocation of a user action.
/// * [CallbackAction], a class for creating an action from a callback. /// * [CallbackAction], a class for creating an action from a callback.
class Shortcuts extends StatefulWidget { class Shortcuts extends StatefulWidget {
/// Creates a const [Shortcuts] widget. /// Creates a const [Shortcuts] widget that owns the map of shortcuts and
/// creates its own manager.
///
/// When using this constructor, [manager] will return null.
/// ///
/// The [child] and [shortcuts] arguments are required. /// The [child] and [shortcuts] arguments are required.
///
/// See also:
///
/// * [Shortcuts.manager], a constructor that uses a [ShortcutManager] to
/// manage the shortcuts list instead.
const Shortcuts({ const Shortcuts({
super.key, super.key,
this.manager, required Map<ShortcutActivator, Intent> shortcuts,
required this.shortcuts, required this.child,
this.debugLabel,
}) : _shortcuts = shortcuts,
manager = null,
assert(shortcuts != null),
assert(child != null);
/// Creates a const [Shortcuts] widget that uses the [manager] to
/// manage the map of shortcuts.
///
/// If this constructor is used, [shortcuts] will return the contents of
/// [ShortcutManager.shortcuts].
///
/// The [child] and [manager] arguments are required.
const Shortcuts.manager({
super.key,
required ShortcutManager this.manager,
required this.child, required this.child,
this.debugLabel, this.debugLabel,
}) : assert(shortcuts != null), }) : _shortcuts = const <ShortcutActivator, Intent>{},
assert(manager != null),
assert(child != null); assert(child != null);
/// The [ShortcutManager] that will manage the mapping between key /// The [ShortcutManager] that will manage the mapping between key
/// combinations and [Action]s. /// combinations and [Action]s.
/// ///
/// If not specified, uses a default-constructed [ShortcutManager]. /// If this widget was created with [Shortcuts.manager], then
/// /// [ShortcutManager.shortcuts] will be used as the source for shortcuts. If
/// This manager will be given new [shortcuts] to manage whenever the /// the unnamed constructor is used, this manager will be null, and a
/// [shortcuts] change materially. /// default-constructed `ShortcutsManager` will be used.
final ShortcutManager? manager; final ShortcutManager? manager;
/// {@template flutter.widgets.shortcuts.shortcuts} /// {@template flutter.widgets.shortcuts.shortcuts}
/// The map of shortcuts that the [ShortcutManager] will be given to manage. /// The map of shortcuts that describes the mapping between a key sequence
/// /// defined by a [ShortcutActivator] and the [Intent] that will be emitted
/// For performance reasons, it is recommended that a pre-built map is passed /// when that key sequence is pressed.
/// in here (e.g. a final variable from your widget class) instead of defining
/// it inline in the build function.
/// {@endtemplate} /// {@endtemplate}
final Map<ShortcutActivator, Intent> shortcuts; Map<ShortcutActivator, Intent> get shortcuts {
return manager == null ? _shortcuts : manager!.shortcuts;
}
final Map<ShortcutActivator, Intent> _shortcuts;
/// The child widget for this [Shortcuts] widget. /// The child widget for this [Shortcuts] widget.
/// ///
...@@ -854,52 +886,6 @@ class Shortcuts extends StatefulWidget { ...@@ -854,52 +886,6 @@ class Shortcuts extends StatefulWidget {
/// unnecessarily with large default shortcut maps. /// unnecessarily with large default shortcut maps.
final String? debugLabel; final String? debugLabel;
/// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext].
///
/// If no [Shortcuts] widget encloses the context given, will assert in debug
/// mode and throw an exception in release mode.
///
/// See also:
///
/// * [maybeOf], which is similar to this function, but will return null if
/// it doesn't find a [Shortcuts] ancestor.
static ShortcutManager of(BuildContext context) {
assert(context != null);
final _ShortcutsMarker? inherited = context.dependOnInheritedWidgetOfExactType<_ShortcutsMarker>();
assert(() {
if (inherited == null) {
throw FlutterError(
'Unable to find a $Shortcuts widget in the context.\n'
'$Shortcuts.of() was called with a context that does not contain a '
'$Shortcuts widget.\n'
'No $Shortcuts ancestor could be found starting from the context that was '
'passed to $Shortcuts.of().\n'
'The context used was:\n'
' $context',
);
}
return true;
}());
return inherited!.manager;
}
/// Returns the [ShortcutManager] that most tightly encloses the given
/// [BuildContext].
///
/// If no [Shortcuts] widget encloses the context given, will return null.
///
/// See also:
///
/// * [of], which is similar to this function, but returns a non-nullable
/// result, and will throw an exception if it doesn't find a [Shortcuts]
/// ancestor.
static ShortcutManager? maybeOf(BuildContext context) {
assert(context != null);
final _ShortcutsMarker? inherited = context.dependOnInheritedWidgetOfExactType<_ShortcutsMarker>();
return inherited?.manager;
}
@override @override
State<Shortcuts> createState() => _ShortcutsState(); State<Shortcuts> createState() => _ShortcutsState();
...@@ -926,8 +912,8 @@ class _ShortcutsState extends State<Shortcuts> { ...@@ -926,8 +912,8 @@ class _ShortcutsState extends State<Shortcuts> {
super.initState(); super.initState();
if (widget.manager == null) { if (widget.manager == null) {
_internalManager = ShortcutManager(); _internalManager = ShortcutManager();
_internalManager!.shortcuts = widget.shortcuts;
} }
manager.shortcuts = widget.shortcuts;
} }
@override @override
...@@ -941,7 +927,7 @@ class _ShortcutsState extends State<Shortcuts> { ...@@ -941,7 +927,7 @@ class _ShortcutsState extends State<Shortcuts> {
_internalManager ??= ShortcutManager(); _internalManager ??= ShortcutManager();
} }
} }
manager.shortcuts = widget.shortcuts; _internalManager?.shortcuts = widget.shortcuts;
} }
KeyEventResult _handleOnKey(FocusNode node, RawKeyEvent event) { KeyEventResult _handleOnKey(FocusNode node, RawKeyEvent event) {
...@@ -1331,8 +1317,7 @@ class _ShortcutRegistrarState extends State<ShortcutRegistrar> { ...@@ -1331,8 +1317,7 @@ class _ShortcutRegistrarState extends State<ShortcutRegistrar> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Shortcuts( return Shortcuts.manager(
shortcuts: registry.shortcuts,
manager: manager, manager: manager,
child: _ShortcutRegistrarMarker( child: _ShortcutRegistrarMarker(
registry: registry, registry: registry,
......
...@@ -532,69 +532,81 @@ void main() { ...@@ -532,69 +532,81 @@ void main() {
group(Shortcuts, () { group(Shortcuts, () {
testWidgets('Default constructed Shortcuts has empty shortcuts', (WidgetTester tester) async { 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(); final ShortcutManager manager = ShortcutManager();
expect(manager.shortcuts, isNotNull); expect(manager.shortcuts, isNotNull);
expect(manager.shortcuts, isEmpty); expect(manager.shortcuts, isEmpty);
const Shortcuts shortcuts = Shortcuts(shortcuts: <LogicalKeySet, Intent>{}, child: SizedBox()); final Shortcuts shortcuts = Shortcuts.manager(manager: manager, child: const SizedBox());
await tester.pumpWidget(shortcuts); await tester.pumpWidget(shortcuts);
expect(shortcuts.shortcuts, isNotNull); expect(shortcuts.shortcuts, isNotNull);
expect(shortcuts.shortcuts, isEmpty); expect(shortcuts.shortcuts, isEmpty);
}); });
testWidgets('Shortcuts.of and maybeOf find shortcuts', (WidgetTester tester) async { 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 GlobalKey containerKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
);
bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
Shortcuts( Actions(
manager: testManager, actions: <Type, Action<Intent>>{
shortcuts: <LogicalKeySet, Intent>{ TestIntent: TestAction(
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(), onInvoke: (Intent intent) {
invoked = true;
return true;
},
),
}, },
child: Focus( child: Shortcuts.manager(
autofocus: true, manager: testManager,
child: SizedBox(key: containerKey, width: 100, height: 100), child: Focus(
autofocus: true,
child: SizedBox(key: containerKey, width: 100, height: 100),
),
), ),
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.maybeOf(containerKey.currentContext!), isNotNull); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(Shortcuts.maybeOf(containerKey.currentContext!), equals(testManager)); expect(invoked, isTrue);
expect(Shortcuts.of(containerKey.currentContext!), equals(testManager)); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
});
testWidgets('Shortcuts.of and maybeOf work correctly without shortcuts', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey();
await tester.pumpWidget(Container(key: containerKey));
expect(Shortcuts.maybeOf(containerKey.currentContext!), isNull);
late FlutterError error;
try {
Shortcuts.of(containerKey.currentContext!);
} on FlutterError catch (e) {
error = e;
} finally {
expect(error, isNotNull);
expect(error.diagnostics.length, 5);
expect(error.diagnostics[2].level, DiagnosticLevel.info);
expect(
error.diagnostics[2].toStringDeep(),
'No Shortcuts ancestor could be found starting from the context\n'
'that was passed to Shortcuts.of().\n',
);
expect(error.toStringDeep(), equalsIgnoringHashCodes(
'FlutterError\n'
' Unable to find a Shortcuts widget in the context.\n'
' Shortcuts.of() was called with a context that does not contain a\n'
' Shortcuts widget.\n'
' No Shortcuts ancestor could be found starting from the context\n'
' that was passed to Shortcuts.of().\n'
' The context used was:\n'
' Container-[GlobalKey#00000]\n',
));
}
}); });
testWidgets('ShortcutManager handles shortcuts', (WidgetTester tester) async { testWidgets('Shortcuts.manager lets manager handle shortcuts', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); bool shortcutsSet = false;
void onShortcutsSet() {
shortcutsSet = true;
}
final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
onShortcutsSet: onShortcutsSet,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
);
shortcutsSet = false;
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
Actions( Actions(
...@@ -606,11 +618,8 @@ void main() { ...@@ -606,11 +618,8 @@ void main() {
}, },
), ),
}, },
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Focus( child: Focus(
autofocus: true, autofocus: true,
child: SizedBox(key: containerKey, width: 100, height: 100), child: SizedBox(key: containerKey, width: 100, height: 100),
...@@ -619,15 +628,20 @@ void main() { ...@@ -619,15 +628,20 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(containerKey.currentContext!), isNotNull);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue); expect(invoked, isTrue);
expect(shortcutsSet, isFalse);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
}); });
testWidgets('ShortcutManager ignores keypresses with no primary focus', (WidgetTester tester) async { testWidgets('ShortcutManager ignores key presses with no primary focus', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
Actions( Actions(
...@@ -639,18 +653,14 @@ void main() { ...@@ -639,18 +653,14 @@ void main() {
}, },
), ),
}, },
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: SizedBox(key: containerKey, width: 100, height: 100), child: SizedBox(key: containerKey, width: 100, height: 100),
), ),
), ),
); );
await tester.pump(); await tester.pump();
expect(primaryFocus, isNull); expect(primaryFocus, isNull);
expect(Shortcuts.of(containerKey.currentContext!), isNotNull);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isFalse); expect(invoked, isFalse);
expect(pressedKeys, isEmpty); expect(pressedKeys, isEmpty);
...@@ -658,14 +668,16 @@ void main() { ...@@ -658,14 +668,16 @@ void main() {
testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async { testWidgets("Shortcuts passes to the next Shortcuts widget if it doesn't map the key", (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
Shortcuts( Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
TestIntent: TestAction( TestIntent: TestAction(
...@@ -688,7 +700,6 @@ void main() { ...@@ -688,7 +700,6 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(containerKey.currentContext!), isNotNull);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isTrue); expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft]));
...@@ -696,15 +707,17 @@ void main() { ...@@ -696,15 +707,17 @@ void main() {
testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async { testWidgets('Shortcuts can disable a shortcut with Intent.doNothing', (WidgetTester tester) async {
final GlobalKey containerKey = GlobalKey(); final GlobalKey containerKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Shortcuts( home: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
TestIntent: TestAction( TestIntent: TestAction(
...@@ -728,7 +741,6 @@ void main() { ...@@ -728,7 +741,6 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(containerKey.currentContext!), isNotNull);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft); await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isFalse); expect(invoked, isFalse);
expect(pressedKeys, isEmpty); expect(pressedKeys, isEmpty);
...@@ -736,22 +748,23 @@ void main() { ...@@ -736,22 +748,23 @@ void main() {
testWidgets("Shortcuts that aren't bound to an action don't absorb keys meant for text fields", (WidgetTester tester) async { 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 GlobalKey textFieldKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
child: TextField(key: textFieldKey, autofocus: true), child: TextField(key: textFieldKey, autofocus: true),
), ),
), ),
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull);
final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); final bool handled = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
expect(handled, isFalse); expect(handled, isFalse);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA]));
...@@ -759,16 +772,18 @@ void main() { ...@@ -759,16 +772,18 @@ void main() {
testWidgets('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async { testWidgets('Shortcuts that are bound to an action do override text fields', (WidgetTester tester) async {
final GlobalKey textFieldKey = GlobalKey(); final GlobalKey textFieldKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
TestIntent: TestAction( TestIntent: TestAction(
...@@ -785,7 +800,6 @@ void main() { ...@@ -785,7 +800,6 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull);
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
expect(result, isTrue); expect(result, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA])); expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.keyA]));
...@@ -794,16 +808,18 @@ void main() { ...@@ -794,16 +808,18 @@ void main() {
testWidgets('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async { testWidgets('Shortcuts can override intents that apply to text fields', (WidgetTester tester) async {
final GlobalKey textFieldKey = GlobalKey(); final GlobalKey textFieldKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
TestIntent: TestAction( TestIntent: TestAction(
...@@ -825,7 +841,6 @@ void main() { ...@@ -825,7 +841,6 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull);
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
expect(result, isFalse); expect(result, isFalse);
expect(invoked, isFalse); expect(invoked, isFalse);
...@@ -833,16 +848,18 @@ void main() { ...@@ -833,16 +848,18 @@ void main() {
testWidgets('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async { testWidgets('Shortcuts can override intents that apply to text fields with DoNothingAndStopPropagationIntent', (WidgetTester tester) async {
final GlobalKey textFieldKey = GlobalKey(); final GlobalKey textFieldKey = GlobalKey();
final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[]; final List<LogicalKeyboardKey> pressedKeys = <LogicalKeyboardKey>[];
final TestShortcutManager testManager = TestShortcutManager(pressedKeys); final TestShortcutManager testManager = TestShortcutManager(
pressedKeys,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
);
bool invoked = false; bool invoked = false;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
child: Shortcuts( child: Shortcuts.manager(
manager: testManager, manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const TestIntent(),
},
child: Actions( child: Actions(
actions: <Type, Action<Intent>>{ actions: <Type, Action<Intent>>{
TestIntent: TestAction( TestIntent: TestAction(
...@@ -864,7 +881,6 @@ void main() { ...@@ -864,7 +881,6 @@ void main() {
), ),
); );
await tester.pump(); await tester.pump();
expect(Shortcuts.of(textFieldKey.currentContext!), isNotNull);
final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA); final bool result = await tester.sendKeyEvent(LogicalKeyboardKey.keyA);
expect(result, isFalse); expect(result, isFalse);
expect(invoked, isFalse); expect(invoked, isFalse);
...@@ -919,11 +935,10 @@ void main() { ...@@ -919,11 +935,10 @@ void main() {
expect(description.length, equals(1)); expect(description.length, equals(1));
expect(description[0], equals('shortcuts: <Debug Label>')); expect(description[0], equals('shortcuts: <Debug Label>'));
}); });
test('Shortcuts diagnostics work when manager specified.', () { test('Shortcuts diagnostics work when manager not specified.', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
Shortcuts( Shortcuts(
manager: ShortcutManager(),
shortcuts: <LogicalKeySet, Intent>{ shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.keyA, LogicalKeyboardKey.keyA,
...@@ -937,11 +952,35 @@ void main() { ...@@ -937,11 +952,35 @@ void main() {
return !node.isFiltered(DiagnosticLevel.info); return !node.isFiltered(DiagnosticLevel.info);
}).map((DiagnosticsNode node) => node.toString()).toList(); }).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.length, equals(2));
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})')); 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}')); expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
}); });
testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async { testWidgets('Shortcuts support multiple intents', (WidgetTester tester) async {
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
bool? value = true; bool? value = true;
...@@ -1787,9 +1826,10 @@ class TestIntent2 extends Intent { ...@@ -1787,9 +1826,10 @@ class TestIntent2 extends Intent {
} }
class TestShortcutManager extends ShortcutManager { class TestShortcutManager extends ShortcutManager {
TestShortcutManager(this.keys); TestShortcutManager(this.keys, { super.shortcuts, this.onShortcutsSet });
List<LogicalKeyboardKey> keys; List<LogicalKeyboardKey> keys;
VoidCallback? onShortcutsSet;
@override @override
KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) { KeyEventResult handleKeypress(BuildContext context, RawKeyEvent event) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment