Add CallbackShortcuts widget (#86045)
This commit is contained in:
parent
cb17425df0
commit
00d9f8df14
@ -1026,6 +1026,8 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
|
|||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
|
/// * [CallbackShortcuts], a less complicated (but less flexible) way of
|
||||||
|
/// defining key bindings that just invoke callbacks.
|
||||||
/// * [Intent], a class for containing a description of a user action to be
|
/// * [Intent], a class for containing a description of a user action to be
|
||||||
/// invoked.
|
/// invoked.
|
||||||
/// * [Action], a class for defining an invocation of a user action.
|
/// * [Action], a class for defining an invocation of a user action.
|
||||||
@ -1197,3 +1199,90 @@ class _ShortcutsMarker extends InheritedNotifier<ShortcutManager> {
|
|||||||
|
|
||||||
ShortcutManager get manager => super.notifier!;
|
ShortcutManager get manager => super.notifier!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A widget that provides an uncomplicated mechanism for binding a key
|
||||||
|
/// combination to a specific callback.
|
||||||
|
///
|
||||||
|
/// This is similar to the functionality provided by the [Shortcuts] widget, but
|
||||||
|
/// instead of requiring a mapping to an [Intent], and an [Actions] widget
|
||||||
|
/// somewhere in the widget tree to bind the [Intent] to, it just takes a set of
|
||||||
|
/// bindings that bind the key combination directly to a [VoidCallback].
|
||||||
|
///
|
||||||
|
/// Because it is a simpler mechanism, it doesn't provide the ability to disable
|
||||||
|
/// the callbacks, or to separate the definition of the shortcuts from the
|
||||||
|
/// definition of the code that is triggered by them (the role that actions play
|
||||||
|
/// in the [Shortcuts]/[Actions] system).
|
||||||
|
///
|
||||||
|
/// However, for some applications the complexity and flexibility of the
|
||||||
|
/// [Shortcuts] and [Actions] mechanism is overkill, and this widget is here for
|
||||||
|
/// those apps.
|
||||||
|
///
|
||||||
|
/// [Shortcuts] and [CallbackShortcuts] can both be used in the same app. As
|
||||||
|
/// with any key handling widget, if this widget handles a key event then
|
||||||
|
/// widgets above it in the focus chain will not receive the event. This means
|
||||||
|
/// that if this widget handles a key, then an ancestor [Shortcuts] widget (or
|
||||||
|
/// any other key handling widget) will not receive that key, and similarly, if
|
||||||
|
/// a descendant of this widget handles the key, then the key event will not
|
||||||
|
/// reach this widget for handling.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
/// * [Focus], a widget that defines which widgets can receive keyboard focus.
|
||||||
|
class CallbackShortcuts extends StatelessWidget {
|
||||||
|
/// Creates a const [CallbackShortcuts] widget.
|
||||||
|
const CallbackShortcuts({
|
||||||
|
Key? key,
|
||||||
|
required this.bindings,
|
||||||
|
required this.child,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// A map of key combinations to callbacks used to define the shortcut
|
||||||
|
/// bindings.
|
||||||
|
///
|
||||||
|
/// If a descendant of this widget has focus, and a key is pressed, the
|
||||||
|
/// activator keys of this map will be asked if they accept the key event. If
|
||||||
|
/// they do, then the corresponding callback is invoked, and the key event
|
||||||
|
/// propagation is halted. If none of the activators accept the key event,
|
||||||
|
/// then the key event continues to be propagated up the focus chain.
|
||||||
|
///
|
||||||
|
/// If more than one activator accepts the key event, then all of the
|
||||||
|
/// callbacks associated with activators that accept the key event are
|
||||||
|
/// invoked.
|
||||||
|
///
|
||||||
|
/// Some examples of [ShortcutActivator] subclasses that can be used to define
|
||||||
|
/// the key combinations here are [SingleActivator], [CharacterActivator], and
|
||||||
|
/// [LogicalKeySet].
|
||||||
|
final Map<ShortcutActivator, VoidCallback> bindings;
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
// A helper function to make the stack trace more useful if the callback
|
||||||
|
// throws, by providing the activator and event as arguments that will appear
|
||||||
|
// in the stack trace.
|
||||||
|
bool _applyKeyBinding(ShortcutActivator activator, RawKeyEvent event) {
|
||||||
|
if (activator.accepts(event, RawKeyboard.instance)) {
|
||||||
|
bindings[activator]!.call();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Focus(
|
||||||
|
canRequestFocus: false,
|
||||||
|
skipTraversal: true,
|
||||||
|
onKey: (FocusNode node, RawKeyEvent event) {
|
||||||
|
KeyEventResult result = KeyEventResult.ignored;
|
||||||
|
// Activates all key bindings that match, returns "handled" if any handle it.
|
||||||
|
for (final ShortcutActivator activator in bindings.keys) {
|
||||||
|
result = _applyKeyBinding(activator, event) ? KeyEventResult.handled : result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1077,4 +1077,171 @@ void main() {
|
|||||||
invoked = 0;
|
invoked = 0;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('CallbackShortcuts', () {
|
||||||
|
testWidgets('trigger on key events', (WidgetTester tester) async {
|
||||||
|
int invoked = 0;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CallbackShortcuts(
|
||||||
|
bindings: <ShortcutActivator, VoidCallback>{
|
||||||
|
const SingleActivator(LogicalKeyboardKey.keyA): () {
|
||||||
|
invoked += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
child: const Focus(
|
||||||
|
autofocus: true,
|
||||||
|
child: Placeholder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA);
|
||||||
|
expect(invoked, equals(1));
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA);
|
||||||
|
expect(invoked, 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user