Unverified Commit 7e1d366a authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Add key event handlers that happen before or after the focus traversal (#136280)

## Description

This adds a mechanism for listening to key events before or after focus traversal occurs.

It adds four methods to the public `FocusManager` API:

- `addEarlyKeyEventHandler` - Adds a handler that can handle events before they are given to the focus tree for handling.
- `removeEarlyKeyEventHandler` - Removes an early event handler.
- `addLateKeyEventHandler` - Adds a handler that can handle events if they have not been handled by anything in the focus tree.
- `removeLateKeyEventHandler` - Removes a late event handler.

This allows an app to get notified for a key anywhere, and prevent the focus tree from seeing that event if it handles it.

For the menu system, this allows it to eat an escape key press and close all the open menus.

## Related Issues
 - https://github.com/flutter/flutter/issues/135334

## Tests
 - Added tests for new functionality.
parent b416473b
......@@ -469,8 +469,16 @@ class _MenuAnchorState extends State<MenuAnchor> {
// Don't just close it on *any* scroll, since we want to be able to scroll
// menus themselves if they're too big for the view.
if (_isRoot) {
_root._close();
_close();
}
}
KeyEventResult _checkForEscape(KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
_close();
return KeyEventResult.handled;
}
return KeyEventResult.ignored;
}
/// Open the menu, optionally at a position relative to the [MenuAnchor].
......@@ -538,6 +546,9 @@ class _MenuAnchorState extends State<MenuAnchor> {
);
});
if (_isRoot) {
FocusManager.instance.addEarlyKeyEventHandler(_checkForEscape);
}
Overlay.of(context).insert(_overlayEntry!);
widget.onOpen?.call();
}
......@@ -551,6 +562,9 @@ class _MenuAnchorState extends State<MenuAnchor> {
if (!_isOpen) {
return;
}
if (_isRoot) {
FocusManager.instance.removeEarlyKeyEventHandler(_checkForEscape);
}
_closeChildren(inDispose: inDispose);
_overlayEntry?.remove();
_overlayEntry?.dispose();
......
......@@ -740,6 +740,9 @@ class Actions extends StatefulWidget {
// getElementForInheritedWidgetOfExactType. Returns true if the visitor found
// what it was looking for.
static bool _visitActionsAncestors(BuildContext context, bool Function(InheritedElement element) visitor) {
if (!context.mounted) {
return false;
}
InheritedElement? actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsScope>();
while (actionsElement != null) {
if (visitor(actionsElement)) {
......
......@@ -118,6 +118,18 @@ typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent
/// was handled.
typedef FocusOnKeyEventCallback = KeyEventResult Function(FocusNode node, KeyEvent event);
/// Signature of a callback used by [FocusManager.addEarlyKeyEventHandler] and
/// [FocusManager.addLateKeyEventHandler].
///
/// The `event` parameter is a [KeyEvent] that is being sent to the callback to
/// be handled.
///
/// The [KeyEventResult] return value indicates whether or not the event will
/// continue to be propagated. If the value returned is [KeyEventResult.handled]
/// or [KeyEventResult.skipRemainingHandlers], then the event will not continue
/// to be propagated.
typedef OnKeyEventCallback = KeyEventResult Function(KeyEvent event);
// Represents a pending autofocus request.
@immutable
class _Autofocus {
......@@ -1548,6 +1560,85 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
/// [FocusManager] notifies.
void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _highlightManager.removeListener(listener);
/// {@template flutter.widgets.focus_manager.FocusManager.addEarlyKeyEventHandler}
/// Adds a key event handler to a set of handlers that are called before any
/// key event handlers in the focus tree are called.
///
/// All of the handlers in the set will be called for every key event the
/// [FocusManager] receives. If any one of the handlers returns
/// [KeyEventResult.handled] or [KeyEventResult.skipRemainingHandlers], then
/// none of the handlers in the focus tree will be called.
///
/// If handlers are added while the existing callbacks are being invoked, they
/// will not be called until the next key event occurs.
///
/// See also:
///
/// * [removeEarlyKeyEventHandler], which removes handlers added by this
/// function.
/// * [addLateKeyEventHandler], which is a similar mechanism for adding
/// handlers that are invoked after the focus tree has had a chance to
/// handle an event.
/// {@endtemplate}
void addEarlyKeyEventHandler(OnKeyEventCallback handler) {
_highlightManager.addEarlyKeyEventHandler(handler);
}
/// {@template flutter.widgets.focus_manager.FocusManager.removeEarlyKeyEventHandler}
/// Removes a key handler added by calling [addEarlyKeyEventHandler].
///
/// If handlers are removed while the existing callbacks are being invoked,
/// they will continue to be called until the next key event is received.
///
/// See also:
///
/// * [addEarlyKeyEventHandler], which adds the handlers removed by this
/// function.
/// {@endtemplate}
void removeEarlyKeyEventHandler(OnKeyEventCallback handler) {
_highlightManager.removeEarlyKeyEventHandler(handler);
}
/// {@template flutter.widgets.focus_manager.FocusManager.addLateKeyEventHandler}
/// Adds a key event handler to a set of handlers that are called if none of
/// the key event handlers in the focus tree handle the event.
///
/// If the event reaches the root of the focus tree without being handled,
/// then all of the handlers in the set will be called. If any of them returns
/// [KeyEventResult.handled] or [KeyEventResult.skipRemainingHandlers], then
/// event propagation to the platform will be stopped.
///
/// If handlers are added while the existing callbacks are being invoked, they
/// will not be called until the next key event is not handled by the focus
/// tree.
///
/// See also:
///
/// * [removeLateKeyEventHandler], which removes handlers added by this
/// function.
/// * [addEarlyKeyEventHandler], which is a similar mechanism for adding
/// handlers that are invoked before the focus tree has had a chance to
/// handle an event.
/// {@endtemplate}
void addLateKeyEventHandler(OnKeyEventCallback handler) {
_highlightManager.addLateKeyEventHandler(handler);
}
/// {@template flutter.widgets.focus_manager.FocusManager.removeLateKeyEventHandler}
/// Removes a key handler added by calling [addLateKeyEventHandler].
///
/// If handlers are removed while the existing callbacks are being invoked,
/// they will continue to be called until the next key event is received.
///
/// See also:
///
/// * [addLateKeyEventHandler], which adds the handlers removed by this
/// function.
/// {@endtemplate}
void removeLateKeyEventHandler(OnKeyEventCallback handler) {
_highlightManager.removeLateKeyEventHandler(handler);
}
/// The root [FocusScopeNode] in the focus tree.
///
/// This field is rarely used directly. To find the nearest [FocusScopeNode]
......@@ -1728,6 +1819,24 @@ class _HighlightModeManager {
updateMode();
}
/// {@macro flutter.widgets.focus_manager.FocusManager.addEarlyKeyEventHandler}
void addEarlyKeyEventHandler(OnKeyEventCallback callback) => _earlyKeyEventHandlers.add(callback);
/// {@macro flutter.widgets.focus_manager.FocusManager.removeEarlyKeyEventHandler}
void removeEarlyKeyEventHandler(OnKeyEventCallback callback) => _earlyKeyEventHandlers.remove(callback);
// The list of callbacks for early key handling.
final HashedObserverList<OnKeyEventCallback> _earlyKeyEventHandlers = HashedObserverList<OnKeyEventCallback>();
/// {@macro flutter.widgets.focus_manager.FocusManager.addLateKeyEventHandler}
void addLateKeyEventHandler(OnKeyEventCallback callback) => _lateKeyEventHandlers.add(callback);
/// {@macro flutter.widgets.focus_manager.FocusManager.removeLateKeyEventHandler}
void removeLateKeyEventHandler(OnKeyEventCallback callback) => _lateKeyEventHandlers.remove(callback);
// The list of callbacks for late key handling.
final HashedObserverList<OnKeyEventCallback> _lateKeyEventHandlers = HashedObserverList<OnKeyEventCallback>();
/// Register a closure to be called when the [FocusManager] notifies its
/// listeners that the value of [highlightMode] has changed.
void addListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener);
......@@ -1819,10 +1928,38 @@ class _HighlightModeManager {
return false;
}
// Walk the current focus from the leaf to the root, calling each one's
bool handled = false;
// Check to see if any of the early handlers handle the key. If so, then
// return early.
if (_earlyKeyEventHandlers.isNotEmpty) {
final List<KeyEventResult> results = <KeyEventResult>[];
// Copy the list before iteration to prevent problems if the list gets
// modified during iteration.
final List<OnKeyEventCallback> iterationList = _earlyKeyEventHandlers.toList();
for (final OnKeyEventCallback callback in iterationList) {
for (final KeyEvent event in message.events) {
results.add(callback(event));
}
}
final KeyEventResult result = combineKeyEventResults(results);
switch (result) {
case KeyEventResult.ignored:
break;
case KeyEventResult.handled:
assert(_focusDebug(() => 'Key event $message handled by early key event callback.'));
handled = true;
case KeyEventResult.skipRemainingHandlers:
assert(_focusDebug(() => 'Key event $message propagation stopped by early key event callback.'));
handled = false;
}
}
if (handled) {
return true;
}
// Walk the current focus from the leaf to the root, calling each node's
// onKey on the way up, and if one responds that they handled it or want to
// stop propagation, stop.
bool handled = false;
for (final FocusNode node in <FocusNode>[
FocusManager.instance.primaryFocus!,
...FocusManager.instance.primaryFocus!.ancestors,
......@@ -1852,8 +1989,32 @@ class _HighlightModeManager {
assert(result != KeyEventResult.ignored);
break;
}
// Check to see if any late key event handlers want to handle the event.
if (!handled && _lateKeyEventHandlers.isNotEmpty) {
final List<KeyEventResult> results = <KeyEventResult>[];
// Copy the list before iteration to prevent problems if the list gets
// modified during iteration.
final List<OnKeyEventCallback> iterationList = _lateKeyEventHandlers.toList();
for (final OnKeyEventCallback callback in iterationList) {
for (final KeyEvent event in message.events) {
results.add(callback(event));
}
}
final KeyEventResult result = combineKeyEventResults(results);
switch (result) {
case KeyEventResult.ignored:
break;
case KeyEventResult.handled:
assert(_focusDebug(() => 'Key event $message handled by late key event callback.'));
handled = true;
case KeyEventResult.skipRemainingHandlers:
assert(_focusDebug(() => 'Key event $message propagation stopped by late key event callback.'));
handled = false;
}
}
if (!handled) {
assert(_focusDebug(() => 'Key event not handled by anyone: $message.'));
assert(_focusDebug(() => 'Key event not handled by focus system: $message.'));
}
return handled;
}
......
......@@ -1784,6 +1784,164 @@ void main() {
);
});
testWidgetsWithLeakTracking('FocusManager.addEarlyKeyEventHandler works', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
addTearDown(focusNode1.dispose);
final List<int> logs = <int>[];
KeyEventResult earlyResult = KeyEventResult.ignored;
KeyEventResult focusResult = KeyEventResult.ignored;
await tester.pumpWidget(
Focus(
focusNode: focusNode1,
onKeyEvent: (_, KeyEvent event) {
logs.add(0);
if (event is KeyDownEvent) {
return focusResult;
}
return KeyEventResult.ignored;
},
onKey: (_, RawKeyEvent event) {
logs.add(1);
if (event is KeyDownEvent) {
return focusResult;
}
return KeyEventResult.ignored;
},
child: const SizedBox(),
),
);
focusNode1.requestFocus();
await tester.pump();
KeyEventResult earlyHandler(KeyEvent event) {
if (event is KeyDownEvent) {
return earlyResult;
}
return KeyEventResult.ignored;
}
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
FocusManager.instance.addEarlyKeyEventHandler(earlyHandler);
logs.clear();
focusResult = KeyEventResult.ignored;
earlyResult = KeyEventResult.handled;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1]);
logs.clear();
focusResult = KeyEventResult.ignored;
earlyResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
logs.clear();
focusResult = KeyEventResult.handled;
earlyResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
FocusManager.instance.removeEarlyKeyEventHandler(earlyHandler);
logs.clear();
focusResult = KeyEventResult.ignored;
earlyResult = KeyEventResult.handled;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
logs.clear();
focusResult = KeyEventResult.handled;
earlyResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgetsWithLeakTracking('FocusManager.addLateKeyEventHandler works', (WidgetTester tester) async {
final FocusNode focusNode1 = FocusNode(debugLabel: 'Test Node 1');
addTearDown(focusNode1.dispose);
final List<int> logs = <int>[];
KeyEventResult lateResult = KeyEventResult.ignored;
KeyEventResult focusResult = KeyEventResult.ignored;
await tester.pumpWidget(
Focus(
focusNode: focusNode1,
onKeyEvent: (_, KeyEvent event) {
logs.add(0);
if (event is KeyDownEvent) {
return focusResult;
}
return KeyEventResult.ignored;
},
onKey: (_, RawKeyEvent event) {
logs.add(1);
if (event is KeyDownEvent) {
return focusResult;
}
return KeyEventResult.ignored;
},
child: const SizedBox(),
),
);
focusNode1.requestFocus();
await tester.pump();
KeyEventResult lateHandler(KeyEvent event) {
if (event is KeyDownEvent) {
return lateResult;
}
return KeyEventResult.ignored;
}
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
FocusManager.instance.addLateKeyEventHandler(lateHandler);
logs.clear();
focusResult = KeyEventResult.ignored;
lateResult = KeyEventResult.handled;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
logs.clear();
focusResult = KeyEventResult.ignored;
lateResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
logs.clear();
focusResult = KeyEventResult.handled;
lateResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
FocusManager.instance.removeLateKeyEventHandler(lateHandler);
logs.clear();
focusResult = KeyEventResult.ignored;
lateResult = KeyEventResult.handled;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), false);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
logs.clear();
focusResult = KeyEventResult.handled;
lateResult = KeyEventResult.ignored;
expect(await simulateKeyDownEvent(LogicalKeyboardKey.digit1), true);
expect(await simulateKeyUpEvent(LogicalKeyboardKey.digit1), false);
expect(logs, <int>[0, 1, 0, 1]);
}, variant: KeySimulatorTransitModeVariant.all());
testWidgetsWithLeakTracking('FocusManager notifies listeners when a widget loses focus because it was removed.', (WidgetTester tester) async {
final FocusNode nodeA = FocusNode(debugLabel: 'a');
addTearDown(nodeA.dispose);
......
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