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

Address previous comments, fix Intent.doNothing. (#41417)

This addresses comments in the original PR (#41245) that introduced Intent.doNothing, adds tests, and fixes an issue with it.
parent b07056b1
...@@ -235,9 +235,9 @@ abstract class UndoableAction extends Action { ...@@ -235,9 +235,9 @@ abstract class UndoableAction extends Action {
@override @override
@mustCallSuper @mustCallSuper
void invoke(FocusNode node, Intent tag) { void invoke(FocusNode node, Intent intent) {
invocationNode = node; invocationNode = node;
invocationIntent = tag; invocationIntent = intent;
} }
@override @override
...@@ -253,8 +253,8 @@ class SetFocusActionBase extends UndoableAction { ...@@ -253,8 +253,8 @@ class SetFocusActionBase extends UndoableAction {
FocusNode _previousFocus; FocusNode _previousFocus;
@override @override
void invoke(FocusNode node, Intent tag) { void invoke(FocusNode node, Intent intent) {
super.invoke(node, tag); super.invoke(node, intent);
_previousFocus = WidgetsBinding.instance.focusManager.primaryFocus; _previousFocus = WidgetsBinding.instance.focusManager.primaryFocus;
node.requestFocus(); node.requestFocus();
} }
...@@ -292,8 +292,8 @@ class SetFocusAction extends SetFocusActionBase { ...@@ -292,8 +292,8 @@ class SetFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(SetFocusAction); static const LocalKey key = ValueKey<Type>(SetFocusAction);
@override @override
void invoke(FocusNode node, Intent tag) { void invoke(FocusNode node, Intent intent) {
super.invoke(node, tag); super.invoke(node, intent);
node.requestFocus(); node.requestFocus();
} }
} }
...@@ -305,8 +305,8 @@ class NextFocusAction extends SetFocusActionBase { ...@@ -305,8 +305,8 @@ class NextFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(NextFocusAction); static const LocalKey key = ValueKey<Type>(NextFocusAction);
@override @override
void invoke(FocusNode node, Intent tag) { void invoke(FocusNode node, Intent intent) {
super.invoke(node, tag); super.invoke(node, intent);
node.nextFocus(); node.nextFocus();
} }
} }
...@@ -317,8 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase { ...@@ -317,8 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(PreviousFocusAction); static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
@override @override
void invoke(FocusNode node, Intent tag) { void invoke(FocusNode node, Intent intent) {
super.invoke(node, tag); super.invoke(node, intent);
node.previousFocus(); node.previousFocus();
} }
} }
...@@ -337,9 +337,9 @@ class DirectionalFocusAction extends SetFocusActionBase { ...@@ -337,9 +337,9 @@ class DirectionalFocusAction extends SetFocusActionBase {
TraversalDirection direction; TraversalDirection direction;
@override @override
void invoke(FocusNode node, DirectionalFocusIntent tag) { void invoke(FocusNode node, DirectionalFocusIntent intent) {
super.invoke(node, tag); super.invoke(node, intent);
final DirectionalFocusIntent args = tag; final DirectionalFocusIntent args = intent;
node.focusInDirection(args.direction); node.focusInDirection(args.direction);
} }
} }
......
...@@ -29,11 +29,10 @@ class Intent extends Diagnosticable { ...@@ -29,11 +29,10 @@ class Intent extends Diagnosticable {
/// An intent that can't be mapped to an action. /// An intent that can't be mapped to an action.
/// ///
/// This Intent is prevented from being mapped to an action in the /// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
/// [ActionDispatcher], and as such can be used to indicate that a shortcut /// so that it can be bound to a key in a [Shortcuts] widget in order to
/// should not do anything, allowing a shortcut defined at a higher level to /// disable a key binding made above it in the hierarchy.
/// be disabled at a deeper level in the widget hierarchy. static const Intent doNothing = DoNothingIntent();
static const Intent doNothing = Intent(ValueKey<Type>(Intent));
/// The key for the action this intent is associated with. /// The key for the action this intent is associated with.
final LocalKey key; final LocalKey key;
...@@ -95,7 +94,7 @@ abstract class Action extends Diagnosticable { ...@@ -95,7 +94,7 @@ abstract class Action extends Diagnosticable {
/// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead. /// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead.
@protected @protected
@mustCallSuper @mustCallSuper
void invoke(FocusNode node, covariant Intent tag); void invoke(FocusNode node, covariant Intent intent);
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
...@@ -134,7 +133,7 @@ class CallbackAction extends Action { ...@@ -134,7 +133,7 @@ class CallbackAction extends Action {
final OnInvokeCallback onInvoke; final OnInvokeCallback onInvoke;
@override @override
void invoke(FocusNode node, Intent tag) => onInvoke.call(node, tag); void invoke(FocusNode node, Intent intent) => onInvoke.call(node, intent);
} }
/// An action manager that simply invokes the actions given to it. /// An action manager that simply invokes the actions given to it.
...@@ -224,6 +223,7 @@ class Actions extends InheritedWidget { ...@@ -224,6 +223,7 @@ class Actions extends InheritedWidget {
// Stop visiting. // Stop visiting.
return false; return false;
} }
element.visitAncestorElements(visitAncestorElement); element.visitAncestorElements(visitAncestorElement);
} }
return dispatcher ?? const ActionDispatcher(); return dispatcher ?? const ActionDispatcher();
...@@ -278,9 +278,6 @@ class Actions extends InheritedWidget { ...@@ -278,9 +278,6 @@ class Actions extends InheritedWidget {
/// ///
/// Setting `nullOk` to true means that if no ambient [Actions] widget is /// Setting `nullOk` to true means that if no ambient [Actions] widget is
/// found, then this method will return false instead of throwing. /// found, then this method will return false instead of throwing.
///
/// If the `intent` argument is [Intent.doNothing], then this function will
/// return false, without looking for a matching action.
static bool invoke( static bool invoke(
BuildContext context, BuildContext context,
Intent intent, { Intent intent, {
...@@ -292,10 +289,6 @@ class Actions extends InheritedWidget { ...@@ -292,10 +289,6 @@ class Actions extends InheritedWidget {
Element actionsElement; Element actionsElement;
Action action; Action action;
if (identical(intent, Intent.doNothing)) {
return false;
}
bool visitAncestorElement(Element element) { bool visitAncestorElement(Element element) {
if (element.widget is! Actions) { if (element.widget is! Actions) {
// Continue visiting. // Continue visiting.
...@@ -358,3 +351,29 @@ class Actions extends InheritedWidget { ...@@ -358,3 +351,29 @@ class Actions extends InheritedWidget {
properties.add(DiagnosticsProperty<Map<LocalKey, ActionFactory>>('actions', actions)); properties.add(DiagnosticsProperty<Map<LocalKey, ActionFactory>>('actions', actions));
} }
} }
/// An [Action], that, as the name implies, does nothing.
///
/// This action is bound to the [Intent.doNothing] intent inside of
/// [WidgetsApp.build] so that a [Shortcuts] widget can bind a key to it to
/// override another shortcut binding defined above it in the hierarchy.
class DoNothingAction extends Action {
/// Const constructor for [DoNothingAction].
const DoNothingAction() : super(key);
/// The Key used for the [DoNothingIntent] intent, and registered at the top
/// level actions in [WidgetsApp.build].
static const LocalKey key = ValueKey<Type>(DoNothingAction);
@override
void invoke(FocusNode node, Intent intent) { }
}
/// An [Intent] that can be used to disable [Shortcuts] key bindings defined
/// above a widget in the hierarchy.
///
/// The [Intent.doNothing] intent is of this type.
class DoNothingIntent extends Intent {
/// Const constructor for [DoNothingIntent].
const DoNothingIntent() : super(DoNothingAction.key);
}
...@@ -8,6 +8,7 @@ import 'dart:collection' show HashMap; ...@@ -8,6 +8,7 @@ import 'dart:collection' show HashMap;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'actions.dart';
import 'banner.dart'; import 'banner.dart';
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
...@@ -1194,14 +1195,19 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv ...@@ -1194,14 +1195,19 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
assert(_debugCheckLocalizations(appLocale)); assert(_debugCheckLocalizations(appLocale));
return DefaultFocusTraversal( return Actions(
policy: ReadingOrderTraversalPolicy(), actions: <LocalKey, ActionFactory>{
child: MediaQuery( DoNothingAction.key: () => const DoNothingAction(),
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), },
child: Localizations( child: DefaultFocusTraversal(
locale: appLocale, policy: ReadingOrderTraversalPolicy(),
delegates: _localizationsDelegates.toList(), child: MediaQuery(
child: title, data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
), ),
), ),
); );
......
...@@ -302,11 +302,11 @@ void main() { ...@@ -302,11 +302,11 @@ void main() {
).debugFillProperties(builder); ).debugFillProperties(builder);
final List<String> description = builder.properties final List<String> description = builder.properties
.where((DiagnosticsNode node) { .where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info); return !node.isFiltered(DiagnosticLevel.info);
}) })
.map((DiagnosticsNode node) => node.toString()) .map((DiagnosticsNode node) => node.toString())
.toList(); .toList();
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
expect(description[1], equals('actions: {}')); expect(description[1], equals('actions: {}'));
......
...@@ -37,22 +37,6 @@ class TestIntent extends Intent { ...@@ -37,22 +37,6 @@ class TestIntent extends Intent {
const TestIntent() : super(TestAction.key); const TestIntent() : super(TestAction.key);
} }
class DoNothingAction extends Action {
const DoNothingAction({
@required OnInvokeCallback onInvoke,
}) : assert(onInvoke != null),
super(key);
static const LocalKey key = ValueKey<Type>(DoNothingAction);
@override
void invoke(FocusNode node, Intent invocation) {}
}
class DoNothingIntent extends Intent {
const DoNothingIntent() : super(DoNothingAction.key);
}
class TestShortcutManager extends ShortcutManager { class TestShortcutManager extends ShortcutManager {
TestShortcutManager(this.keys); TestShortcutManager(this.keys);
...@@ -143,8 +127,8 @@ void main() { ...@@ -143,8 +127,8 @@ void main() {
LogicalKeyboardKey.keyD, LogicalKeyboardKey.keyD,
LogicalKeyboardKey.keyC, LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyB, LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyA,} LogicalKeyboardKey.keyA,
); });
final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'}; final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'};
expect(set2 == set3, isTrue); expect(set2 == set3, isTrue);
expect(set2 == set4, isTrue); expect(set2 == set4, isTrue);
...@@ -192,10 +176,12 @@ void main() { ...@@ -192,10 +176,12 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Actions( Actions(
actions: <LocalKey, ActionFactory>{ actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) { TestAction.key: () => TestAction(
invoked = true; onInvoke: (FocusNode node, Intent intent) {
return true; invoked = true;
}), return true;
},
),
}, },
child: Shortcuts( child: Shortcuts(
manager: testManager, manager: testManager,
...@@ -228,14 +214,16 @@ void main() { ...@@ -228,14 +214,16 @@ void main() {
}, },
child: Actions( child: Actions(
actions: <LocalKey, ActionFactory>{ actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) { TestAction.key: () => TestAction(
invoked = true; onInvoke: (FocusNode node, Intent intent) {
return true; invoked = true;
}), return true;
},
),
}, },
child: Shortcuts( child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{ shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingIntent(), LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing,
}, },
child: Focus( child: Focus(
autofocus: true, autofocus: true,
...@@ -251,5 +239,45 @@ void main() { ...@@ -251,5 +239,45 @@ void main() {
expect(invoked, isTrue); expect(invoked, isTrue);
expect(pressedKeys, equals(<LogicalKeyboardKey>[LogicalKeyboardKey.shiftLeft])); 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);
bool invoked = false;
await tester.pumpWidget(
MaterialApp(
home: Shortcuts(
manager: testManager,
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
invoked = true;
return true;
},
),
},
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.shift): Intent.doNothing,
},
child: Focus(
autofocus: true,
child: Container(key: containerKey, width: 100, height: 100),
),
),
),
),
),
);
await tester.pump();
expect(Shortcuts.of(containerKey.currentContext), isNotNull);
await tester.sendKeyDownEvent(LogicalKeyboardKey.shiftLeft);
expect(invoked, isFalse);
expect(pressedKeys, isEmpty);
});
}); });
} }
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