Overridable default platform key bindings (#45102)

This adds actions and shortcuts arguments to WidgetsApp (and MaterialApp and CupertinoApp) to allow developers to override the default mappings on an application, and to allow for a more complex definition of the default mappings.

I've stopped using SelectAction here, in favor of using ActivateAction for all activations, but haven't removed it, to avoid a breaking change, and to allow a common base class for these types of actions. This is because some platforms use the same mapping (web) for both kinds of activations (both select and activate).
This commit is contained in:
Greg Spencer 2019-12-04 16:07:01 -08:00 committed by GitHub
parent 9011cece25
commit f7d1616173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 416 additions and 118 deletions

View File

@ -91,6 +91,8 @@ class CupertinoApp extends StatefulWidget {
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(title != null),
@ -192,6 +194,67 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
final bool debugShowCheckedModeBanner;
/// {@macro flutter.widgets.widgetsApp.shortcuts}
/// {@tool sample}
/// This example shows how to add a single shortcut for
/// [LogicalKeyboardKey.select] to the default shortcuts without needing to
/// add your own [Shortcuts] widget.
///
/// Alternatively, you could insert a [Shortcuts] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
final Map<LogicalKeySet, Intent> shortcuts;
/// {@macro flutter.widgets.widgetsApp.actions}
/// {@tool sample}
/// This example shows how to add a single action handling an
/// [ActivateAction] to the default actions without needing to
/// add your own [Actions] widget.
///
/// Alternatively, you could insert a [Actions] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// // Do something here...
/// },
/// ),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<LocalKey, ActionFactory> actions;
@override
_CupertinoAppState createState() => _CupertinoAppState();
@ -312,6 +375,8 @@ class _CupertinoAppState extends State<CupertinoApp> {
onPressed: onPressed,
);
},
shortcuts: widget.shortcuts,
actions: widget.actions,
);
},
),

View File

@ -189,6 +189,8 @@ class MaterialApp extends StatefulWidget {
this.checkerboardOffscreenLayers = false,
this.showSemanticsDebugger = false,
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(title != null),
@ -455,6 +457,67 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
final bool debugShowCheckedModeBanner;
/// {@macro flutter.widgets.widgetsApp.shortcuts}
/// {@tool sample}
/// This example shows how to add a single shortcut for
/// [LogicalKeyboardKey.select] to the default shortcuts without needing to
/// add your own [Shortcuts] widget.
///
/// Alternatively, you could insert a [Shortcuts] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
final Map<LogicalKeySet, Intent> shortcuts;
/// {@macro flutter.widgets.widgetsApp.actions}
/// {@tool sample}
/// This example shows how to add a single action handling an
/// [ActivateAction] to the default actions without needing to
/// add your own [Actions] widget.
///
/// Alternatively, you could insert a [Actions] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// // Do something here...
/// },
/// ),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<LocalKey, ActionFactory> actions;
/// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps.
///
@ -626,6 +689,8 @@ class _MaterialAppState extends State<MaterialApp> {
mini: true,
);
},
shortcuts: widget.shortcuts,
actions: widget.actions,
);
assert(() {

View File

@ -164,8 +164,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
ActivateAction.key: _createAction,
};
}
@ -189,7 +188,7 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
Action _createAction() {
return CallbackAction(
SelectAction.key,
ActivateAction.key,
onInvoke: _actionHandler,
);
}

View File

@ -152,7 +152,7 @@ class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>>
}
static final Map<LogicalKeySet, Intent> _webShortcuts =<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(SelectAction.key),
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
};
@override
@ -1059,8 +1059,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
_internalNode ??= _createFocusNode();
}
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
ActivateAction.key: _createAction,
};
focusNode.addListener(_handleFocusChanged);
final FocusManager focusManager = WidgetsBinding.instance.focusManager;

View File

@ -505,8 +505,7 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
ActivateAction.key: _createAction,
};
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
}

View File

@ -192,8 +192,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
ActivateAction.key: _createAction,
};
}
@ -207,7 +206,7 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
Action _createAction() {
return CallbackAction(
SelectAction.key,
ActivateAction.key,
onInvoke: _actionHandler,
);
}

View File

@ -219,8 +219,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
SelectAction.key: _createAction,
if (!kIsWeb) ActivateAction.key: _createAction,
ActivateAction.key: _createAction,
};
}
@ -234,7 +233,7 @@ class _SwitchState extends State<Switch> with TickerProviderStateMixin {
Action _createAction() {
return CallbackAction(
SelectAction.key,
ActivateAction.key,
onInvoke: _actionHandler,
);
}

View File

@ -738,8 +738,7 @@ abstract class ActivateAction extends Action {
/// An action that selects the currently focused control.
///
/// This is an abstract class that serves as a base class for actions that
/// select something, like a checkbox or a radio button. By default, it is bound
/// to [LogicalKeyboardKey.space] in the default keyboard map in [WidgetsApp].
/// select something. It is not bound to any key by default.
abstract class SelectAction extends Action {
/// Creates a [SelectAction] with a fixed [key];
const SelectAction() : super(key);

View File

@ -4,7 +4,6 @@
import 'dart:async';
import 'dart:collection' show HashMap;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
@ -170,6 +169,8 @@ class WidgetsApp extends StatefulWidget {
this.debugShowWidgetInspector = false,
this.debugShowCheckedModeBanner = true,
this.inspectorSelectButtonBuilder,
this.shortcuts,
this.actions,
}) : assert(navigatorObservers != null),
assert(routes != null),
assert(
@ -682,6 +683,103 @@ class WidgetsApp extends StatefulWidget {
/// {@endtemplate}
final bool debugShowCheckedModeBanner;
/// {@template flutter.widgets.widgetsApp.shortcuts}
/// The default map of keyboard shortcuts to intents for the application.
///
/// By default, this is set to [WidgetsApp.defaultShortcuts].
/// {@endtemplate}
///
/// {@tool sample}
/// This example shows how to add a single shortcut for
/// [LogicalKeyboardKey.select] to the default shortcuts without needing to
/// add your own [Shortcuts] widget.
///
/// Alternatively, you could insert a [Shortcuts] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@template flutter.widgets.widgetsApp.shortcuts.seeAlso}
/// See also:
///
/// * [LogicalKeySet], a set of [LogicalKeyboardKey]s that make up the keys
/// for this map.
/// * The [Shortcuts] widget, which defines a keyboard mapping.
/// * The [Actions] widget, which defines the mapping from intent to action.
/// * The [Intent] and [Action] classes, which allow definition of new
/// actions.
/// {@endtemplate}
final Map<LogicalKeySet, Intent> shortcuts;
/// {@template flutter.widgets.widgetsApp.actions}
/// The default map of intent keys to actions for the application.
///
/// By default, this is the output of [WidgetsApp.defaultActions], called with
/// [defaultTargetPlatform]. Specifying [actions] for an app overrides the
/// default, so if you wish to modify the default [actions], you can call
/// [WidgetsApp.defaultActions] and modify the resulting map, passing it as
/// the [actions] for this app. You may also add to the bindings, or override
/// specific bindings for a widget subtree, by adding your own [Actions]
/// widget.
/// {@endtemplate}
///
/// {@tool sample}
/// This example shows how to add a single action handling an
/// [ActivateAction] to the default actions without needing to
/// add your own [Actions] widget.
///
/// Alternatively, you could insert a [Actions] widget with just the mapping
/// you want to add between the [WidgetsApp] and its child and get the same
/// effect.
///
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// // Do something here...
/// },
/// ),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
/// return const Placeholder();
/// },
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@template flutter.widgets.widgetsApp.actions.seeAlso}
/// See also:
///
/// * The [shortcuts] parameter, which defines the default set of shortcuts
/// for the application.
/// * The [Shortcuts] widget, which defines a keyboard mapping.
/// * The [Actions] widget, which defines the mapping from intent to action.
/// * The [Intent] and [Action] classes, which allow definition of new
/// actions.
/// {@endtemplate}
final Map<LocalKey, ActionFactory> actions;
/// If true, forces the performance overlay to be visible in all instances.
///
/// Used by the `showPerformanceOverlay` observatory extension.
@ -705,6 +803,101 @@ class WidgetsApp extends StatefulWidget {
/// with "s".
static bool debugAllowBannerOverride = true;
static final Map<LogicalKeySet, Intent> _defaultShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
// Keyboard traversal.
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
// Scrolling
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
LogicalKeySet(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
};
// Default shortcuts for the web platform.
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
// Keyboard traversal.
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
// Scrolling
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
LogicalKeySet(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
};
// Default shortcuts for the macOS platform.
static final Map<LogicalKeySet, Intent> _defaultMacOsShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
// Keyboard traversal
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up),
// Scrolling
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
LogicalKeySet(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
};
/// Generates the default shortcut key bindings based on the
/// [defaultTargetPlatform].
///
/// Used by [WidgetsApp] to assign a default value to [WidgetsApp.shortcuts].
static Map<LogicalKeySet, Intent> get defaultShortcuts {
if (kIsWeb) {
return _defaultWebShortcuts;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return _defaultShortcuts;
case TargetPlatform.macOS:
return _defaultMacOsShortcuts;
case TargetPlatform.iOS:
// No keyboard support on iOS yet.
break;
}
return <LogicalKeySet, Intent>{};
}
/// The default value of [WidgetsApp.actions].
static final Map<LocalKey, ActionFactory> defaultActions = <LocalKey, ActionFactory>{
DoNothingAction.key: () => const DoNothingAction(),
RequestFocusAction.key: () => RequestFocusAction(),
NextFocusAction.key: () => NextFocusAction(),
PreviousFocusAction.key: () => PreviousFocusAction(),
DirectionalFocusAction.key: () => DirectionalFocusAction(),
ScrollAction.key: () => ScrollAction(),
};
@override
_WidgetsAppState createState() => _WidgetsAppState();
}
@ -818,7 +1011,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return true;
}
// LOCALIZATION
/// This is the resolved locale, and is one of the supportedLocales.
@ -1042,60 +1234,6 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
return true;
}
final Map<LogicalKeySet, Intent> _keyMap = <LogicalKeySet, Intent>{
// Next/previous keyboard traversal.
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
// Directional keyboard traversal. Not available on web.
if (!kIsWeb) ...<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowUp): const DirectionalFocusIntent(TraversalDirection.up)
},
// Keyboard scrolling.
// TODO(gspencergoog): Convert all of the Platform.isMacOS checks to be
// defaultTargetPlatform == TargetPlatform.macOS, once that exists.
// https://github.com/flutter/flutter/issues/31366
if (!kIsWeb && !Platform.isMacOS) ...<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
},
if (!kIsWeb && Platform.isMacOS) ...<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.meta, LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
},
// Web scrolling.
if (kIsWeb) ...<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const ScrollIntent(direction: AxisDirection.down),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const ScrollIntent(direction: AxisDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const ScrollIntent(direction: AxisDirection.right),
},
LogicalKeySet(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
LogicalKeySet(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
};
final Map<LocalKey, ActionFactory> _actionMap = <LocalKey, ActionFactory>{
DoNothingAction.key: () => const DoNothingAction(),
RequestFocusAction.key: () => RequestFocusAction(),
NextFocusAction.key: () => NextFocusAction(),
PreviousFocusAction.key: () => PreviousFocusAction(),
DirectionalFocusAction.key: () => DirectionalFocusAction(),
ScrollAction.key: () => ScrollAction(),
};
@override
Widget build(BuildContext context) {
Widget navigator;
@ -1207,9 +1345,9 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
assert(_debugCheckLocalizations(appLocale));
return Shortcuts(
shortcuts: _keyMap,
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
child: Actions(
actions: _actionMap,
actions: widget.actions ?? WidgetsApp.defaultActions,
child: DefaultFocusTraversal(
policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow(

View File

@ -322,46 +322,12 @@ void main() {
);
}
// Now activate it with a keypress.
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
if (kIsWeb) {
expect(box, isNot(ripplePattern(30.0, 0)));
} else {
// ripplePattern always add a translation of topLeft.
expect(box, ripplePattern(30.0, 0));
// The ripple fades in for 75ms. During that time its alpha is eased from
// 0 to the splashColor's alpha value.
await tester.pump(const Duration(milliseconds: 50));
expect(box, ripplePattern(56.0, 120));
// At 75ms the ripple has faded in: it's alpha matches the splashColor's
// alpha.
await tester.pump(const Duration(milliseconds: 25));
expect(box, ripplePattern(73.0, 180));
// At this point the splash radius has expanded to its limit: 5 past the
// ink well's radius parameter. The fade-out is about to start.
// The fade-out begins at 225ms = 50ms + 25ms + 150ms.
await tester.pump(const Duration(milliseconds: 150));
expect(box, ripplePattern(105.0, 180));
// After another 150ms the fade-out is complete.
await tester.pump(const Duration(milliseconds: 150));
expect(box, ripplePattern(105.0, 0));
}
// Now try it with a select action instead.
await buildTest(SelectAction.key);
await buildTest(ActivateAction.key);
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
final RenderBox box = Material.of(tester.element(find.byType(InkWell))) as dynamic;
// ripplePattern always add a translation of topLeft.
expect(box, ripplePattern(30.0, 0));

View File

@ -48,7 +48,7 @@ void main() {
Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(SelectAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
},
child: Directionality(
textDirection: TextDirection.ltr,
@ -78,7 +78,7 @@ void main() {
await tester.pumpAndSettle();
expect(pressed, kIsWeb ? isFalse : isTrue);
expect(pressed, isTrue);
pressed = false;
await tester.sendKeyEvent(LogicalKeyboardKey.space);

View File

@ -3,9 +3,23 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class TestAction extends Action {
TestAction() : super(key);
static const LocalKey key = ValueKey<Type>(TestAction);
int calls = 0;
@override
void invoke(FocusNode node, Intent intent) {
calls += 1;
}
}
void main() {
testWidgets('WidgetsApp with builder only', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
@ -21,6 +35,67 @@ void main() {
expect(find.byKey(key), findsOneWidget);
});
testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async {
bool checked = false;
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
WidgetsApp(
key: key,
builder: (BuildContext context, Widget child) {
return Material(
child: Checkbox(
value: checked,
autofocus: true,
onChanged: (bool value) {
checked = value;
},
),
);
},
color: const Color(0xFF123456),
),
);
await tester.pump(); // Wait for focus to take effect.
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
// Default key mapping worked.
expect(checked, isTrue);
checked = false;
final TestAction action = TestAction();
await tester.pumpWidget(
WidgetsApp(
key: key,
actions: <LocalKey, ActionFactory>{
TestAction.key: () => action,
},
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.space): const Intent(TestAction.key),
},
builder: (BuildContext context, Widget child) {
return Material(
child: Checkbox(
value: checked,
autofocus: true,
onChanged: (bool value) {
checked = value;
},
),
);
},
color: const Color(0xFF123456),
),
);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
// Default key mapping was not invoked.
expect(checked, isFalse);
// Overridden mapping was invoked.
expect(action.calls, equals(1));
});
group('error control test', () {
Future<void> expectFlutterError({
GlobalKey<NavigatorState> key,

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
@ -36,11 +35,7 @@ Future<void> pumpTest(
const double dragOffset = 200.0;
// TODO(gspencergoog): Change this to use TargetPlatform.macOS once that is available.
// https://github.com/flutter/flutter/issues/31366
// Can't be const, since Platform.macOS asserts if called in const context.
// ignore: prefer_const_declarations
final LogicalKeyboardKey modifierKey = (!kIsWeb && Platform.isMacOS)
final LogicalKeyboardKey modifierKey = defaultTargetPlatform == TargetPlatform.macOS
? LogicalKeyboardKey.metaLeft
: LogicalKeyboardKey.controlLeft;