Skip to content

Commit

Permalink
Address previous comments, fix Intent.doNothing. (flutter#41417)
Browse files Browse the repository at this point in the history
This addresses comments in the original PR (flutter#41245) that introduced Intent.doNothing, adds tests, and fixes an issue with it.
  • Loading branch information
gspencergoog committed Sep 27, 2019
1 parent b07056b commit eb3e2f5
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 67 deletions.
26 changes: 13 additions & 13 deletions dev/manual_tests/lib/actions.dart
Expand Up @@ -235,9 +235,9 @@ abstract class UndoableAction extends Action {

@override
@mustCallSuper
void invoke(FocusNode node, Intent tag) {
void invoke(FocusNode node, Intent intent) {
invocationNode = node;
invocationIntent = tag;
invocationIntent = intent;
}

@override
Expand All @@ -253,8 +253,8 @@ class SetFocusActionBase extends UndoableAction {
FocusNode _previousFocus;

@override
void invoke(FocusNode node, Intent tag) {
super.invoke(node, tag);
void invoke(FocusNode node, Intent intent) {
super.invoke(node, intent);
_previousFocus = WidgetsBinding.instance.focusManager.primaryFocus;
node.requestFocus();
}
Expand Down Expand Up @@ -292,8 +292,8 @@ class SetFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(SetFocusAction);

@override
void invoke(FocusNode node, Intent tag) {
super.invoke(node, tag);
void invoke(FocusNode node, Intent intent) {
super.invoke(node, intent);
node.requestFocus();
}
}
Expand All @@ -305,8 +305,8 @@ class NextFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(NextFocusAction);

@override
void invoke(FocusNode node, Intent tag) {
super.invoke(node, tag);
void invoke(FocusNode node, Intent intent) {
super.invoke(node, intent);
node.nextFocus();
}
}
Expand All @@ -317,8 +317,8 @@ class PreviousFocusAction extends SetFocusActionBase {
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);

@override
void invoke(FocusNode node, Intent tag) {
super.invoke(node, tag);
void invoke(FocusNode node, Intent intent) {
super.invoke(node, intent);
node.previousFocus();
}
}
Expand All @@ -337,9 +337,9 @@ class DirectionalFocusAction extends SetFocusActionBase {
TraversalDirection direction;

@override
void invoke(FocusNode node, DirectionalFocusIntent tag) {
super.invoke(node, tag);
final DirectionalFocusIntent args = tag;
void invoke(FocusNode node, DirectionalFocusIntent intent) {
super.invoke(node, intent);
final DirectionalFocusIntent args = intent;
node.focusInDirection(args.direction);
}
}
Expand Down
47 changes: 33 additions & 14 deletions packages/flutter/lib/src/widgets/actions.dart
Expand Up @@ -29,11 +29,10 @@ class Intent extends Diagnosticable {

/// An intent that can't be mapped to an action.
///
/// This Intent is prevented from being mapped to an action in the
/// [ActionDispatcher], and as such can be used to indicate that a shortcut
/// should not do anything, allowing a shortcut defined at a higher level to
/// be disabled at a deeper level in the widget hierarchy.
static const Intent doNothing = Intent(ValueKey<Type>(Intent));
/// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
/// so that it can be bound to a key in a [Shortcuts] widget in order to
/// disable a key binding made above it in the hierarchy.
static const Intent doNothing = DoNothingIntent();

/// The key for the action this intent is associated with.
final LocalKey key;
Expand Down Expand Up @@ -95,7 +94,7 @@ abstract class Action extends Diagnosticable {
/// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead.
@protected
@mustCallSuper
void invoke(FocusNode node, covariant Intent tag);
void invoke(FocusNode node, covariant Intent intent);

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
Expand Down Expand Up @@ -134,7 +133,7 @@ class CallbackAction extends Action {
final OnInvokeCallback onInvoke;

@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.
Expand Down Expand Up @@ -224,6 +223,7 @@ class Actions extends InheritedWidget {
// Stop visiting.
return false;
}

element.visitAncestorElements(visitAncestorElement);
}
return dispatcher ?? const ActionDispatcher();
Expand Down Expand Up @@ -278,9 +278,6 @@ class Actions extends InheritedWidget {
///
/// Setting `nullOk` to true means that if no ambient [Actions] widget is
/// 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(
BuildContext context,
Intent intent, {
Expand All @@ -292,10 +289,6 @@ class Actions extends InheritedWidget {
Element actionsElement;
Action action;

if (identical(intent, Intent.doNothing)) {
return false;
}

bool visitAncestorElement(Element element) {
if (element.widget is! Actions) {
// Continue visiting.
Expand Down Expand Up @@ -358,3 +351,29 @@ class Actions extends InheritedWidget {
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);
}
22 changes: 14 additions & 8 deletions packages/flutter/lib/src/widgets/app.dart
Expand Up @@ -8,6 +8,7 @@ import 'dart:collection' show HashMap;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';

import 'actions.dart';
import 'banner.dart';
import 'basic.dart';
import 'binding.dart';
Expand Down Expand Up @@ -1194,14 +1195,19 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv

assert(_debugCheckLocalizations(appLocale));

return DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
return Actions(
actions: <LocalKey, ActionFactory>{
DoNothingAction.key: () => const DoNothingAction(),
},
child: DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
),
),
);
Expand Down
10 changes: 5 additions & 5 deletions packages/flutter/test/widgets/actions_test.dart
Expand Up @@ -302,11 +302,11 @@ void main() {
).debugFillProperties(builder);

final List<String> description = builder.properties
.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
})
.map((DiagnosticsNode node) => node.toString())
.toList();
.where((DiagnosticsNode node) {
return !node.isFiltered(DiagnosticLevel.info);
})
.map((DiagnosticsNode node) => node.toString())
.toList();

expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
expect(description[1], equals('actions: {}'));
Expand Down
82 changes: 55 additions & 27 deletions packages/flutter/test/widgets/shortcuts_test.dart
Expand Up @@ -37,22 +37,6 @@ class TestIntent extends Intent {
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 {
TestShortcutManager(this.keys);

Expand Down Expand Up @@ -143,8 +127,8 @@ void main() {
LogicalKeyboardKey.keyD,
LogicalKeyboardKey.keyC,
LogicalKeyboardKey.keyB,
LogicalKeyboardKey.keyA,}
);
LogicalKeyboardKey.keyA,
});
final Map<LogicalKeySet, String> map = <LogicalKeySet, String>{set1: 'one'};
expect(set2 == set3, isTrue);
expect(set2 == set4, isTrue);
Expand Down Expand Up @@ -192,10 +176,12 @@ void main() {
await tester.pumpWidget(
Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) {
invoked = true;
return true;
}),
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
invoked = true;
return true;
},
),
},
child: Shortcuts(
manager: testManager,
Expand Down Expand Up @@ -228,14 +214,16 @@ void main() {
},
child: Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(onInvoke: (FocusNode node, Intent intent) {
invoked = true;
return true;
}),
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
invoked = true;
return true;
},
),
},
child: Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.keyA): const DoNothingIntent(),
LogicalKeySet(LogicalKeyboardKey.keyA): Intent.doNothing,
},
child: Focus(
autofocus: true,
Expand All @@ -251,5 +239,45 @@ void main() {
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);
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);
});
});
}

0 comments on commit eb3e2f5

Please sign in to comment.