Unverified Commit 0f68b46f authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Revise Action API (#42940)

This updates the Action API in accordance with the design doc for the changes: flutter.dev/go/actions-and-shortcuts-design-revision

Fixes #53276
parent c663cd55
This diff is collapsed.
......@@ -213,7 +213,7 @@ class CupertinoApp extends StatefulWidget {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
......@@ -239,12 +239,12 @@ class CupertinoApp extends StatefulWidget {
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// actions: <Type, Action<Intent>>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// ActivateAction: CallbackAction(
/// onInvoke: (Intent intent) {
/// // Do something here...
/// return null;
/// },
/// ),
/// },
......@@ -257,7 +257,7 @@ class CupertinoApp extends StatefulWidget {
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<LocalKey, ActionFactory> actions;
final Map<Type, Action<Intent>> actions;
@override
_CupertinoAppState createState() => _CupertinoAppState();
......
......@@ -183,9 +183,9 @@ class ChangeNotifier implements Listenable {
/// Call all the registered listeners.
///
/// Call this method whenever the object changes, to notify any clients the
/// object may have. Listeners that are added during this iteration will not
/// be visited. Listeners that are removed during this iteration will not be
/// visited after they are removed.
/// object may have changed. Listeners that are added during this iteration
/// will not be visited. Listeners that are removed during this iteration will
/// not be visited after they are removed.
///
/// Exceptions thrown by listeners will be caught and reported using
/// [FlutterError.reportError].
......
......@@ -476,7 +476,7 @@ class MaterialApp extends StatefulWidget {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
......@@ -502,12 +502,12 @@ class MaterialApp extends StatefulWidget {
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// actions: <Type, Action<Intent>>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// ActivateAction: CallbackAction(
/// onInvoke: (Intent intent) {
/// // Do something here...
/// return null;
/// },
/// ),
/// },
......@@ -520,7 +520,7 @@ class MaterialApp extends StatefulWidget {
/// ```
/// {@end-tool}
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<LocalKey, ActionFactory> actions;
final Map<Type, Action<Intent>> actions;
/// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps.
......
......@@ -169,17 +169,17 @@ class Checkbox extends StatefulWidget {
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
ActivateAction.key: _createAction,
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
}
void _actionHandler(FocusNode node, Intent intent){
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
switch (widget.value) {
case false:
......@@ -193,17 +193,10 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
break;
}
}
final RenderObject renderObject = node.context.findRenderObject();
final RenderObject renderObject = context.findRenderObject();
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: _actionHandler,
);
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
......
......@@ -158,7 +158,7 @@ class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>>
}
static final Map<LogicalKeySet, Intent> _webShortcuts =<LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
};
@override
......@@ -1080,7 +1080,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
FocusNode _internalNode;
FocusNode get focusNode => widget.focusNode ?? _internalNode;
bool _hasPrimaryFocus = false;
Map<LocalKey, ActionFactory> _actionMap;
Map<Type, Action<Intent>> _actionMap;
FocusHighlightMode _focusHighlightMode;
// Only used if needed to create _internalNode.
......@@ -1095,8 +1095,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
if (widget.focusNode == null) {
_internalNode ??= _createFocusNode();
}
_actionMap = <LocalKey, ActionFactory>{
ActivateAction.key: _createAction,
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: (ActivateIntent intent) => _handleTap(),
),
};
focusNode.addListener(_handleFocusChanged);
final FocusManager focusManager = WidgetsBinding.instance.focusManager;
......@@ -1225,15 +1227,6 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
}
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: (FocusNode node, Intent intent) {
_handleTap();
},
);
}
// When isDense is true, reduce the height of this button from _kMenuItemHeight to
// _kDenseButtonHeight, but don't make it smaller than the text that it contains.
// Similarly, we don't reduce the height of the button so much that its icon
......
......@@ -559,27 +559,20 @@ class _InkResponseState<T extends InkResponse> extends State<T> with AutomaticKe
InteractiveInkFeature _currentSplash;
bool _hovering = false;
final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
Map<LocalKey, ActionFactory> _actionMap;
Map<Type, Action<Intent>> _actionMap;
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
void _handleAction(FocusNode node, Intent intent) {
_startSplash(context: node.context);
_handleTap(node.context);
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: _handleAction,
);
void _handleAction(ActivateIntent intent) {
_startSplash(context: context);
_handleTap(context);
}
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
ActivateAction.key: _createAction,
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleAction),
};
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
}
......
......@@ -262,31 +262,26 @@ class Radio<T> extends StatefulWidget {
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
bool get enabled => widget.onChanged != null;
Map<LocalKey, ActionFactory> _actionMap;
Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
ActivateAction.key: _createAction,
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(
onInvoke: _actionHandler,
),
};
}
void _actionHandler(FocusNode node, Intent intent) {
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
widget.onChanged(widget.value);
}
final RenderObject renderObject = node.context.findRenderObject();
final RenderObject renderObject = context.findRenderObject();
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: _actionHandler,
);
}
bool _focused = false;
void _handleHighlightChanged(bool focused) {
if (_focused != focused) {
......
......@@ -230,31 +230,24 @@ class Switch extends StatefulWidget {
}
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
Map<LocalKey, ActionFactory> _actionMap;
Map<Type, Action<Intent>> _actionMap;
@override
void initState() {
super.initState();
_actionMap = <LocalKey, ActionFactory>{
ActivateAction.key: _createAction,
_actionMap = <Type, Action<Intent>>{
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
};
}
void _actionHandler(FocusNode node, Intent intent){
void _actionHandler(ActivateIntent intent) {
if (widget.onChanged != null) {
widget.onChanged(!widget.value);
}
final RenderObject renderObject = node.context.findRenderObject();
final RenderObject renderObject = context.findRenderObject();
renderObject.sendSemanticsEvent(const TapSemanticEvent());
}
Action _createAction() {
return CallbackAction(
ActivateAction.key,
onInvoke: _actionHandler,
);
}
bool _focused = false;
void _handleFocusHighlightChanged(bool focused) {
if (focused != _focused) {
......
......@@ -78,7 +78,7 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;
/// shortcuts: <LogicalKeySet, Intent>{
/// // Pressing enter on the field will now move to the next field.
/// LogicalKeySet(LogicalKeyboardKey.enter):
/// Intent(NextFocusAction.key),
/// NextFocusIntent(),
/// },
/// child: FocusTraversalGroup(
/// child: Form(
......
......@@ -743,7 +743,7 @@ class WidgetsApp extends StatefulWidget {
/// return WidgetsApp(
/// shortcuts: <LogicalKeySet, Intent>{
/// ... WidgetsApp.defaultShortcuts,
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
/// },
/// color: const Color(0xFFFF0000),
/// builder: (BuildContext context, Widget child) {
......@@ -790,12 +790,12 @@ class WidgetsApp extends StatefulWidget {
/// ```dart
/// Widget build(BuildContext context) {
/// return WidgetsApp(
/// actions: <LocalKey, ActionFactory>{
/// actions: <Type, Action<Intent>>{
/// ... WidgetsApp.defaultActions,
/// ActivateAction.key: () => CallbackAction(
/// ActivateAction.key,
/// onInvoke: (FocusNode focusNode, Intent intent) {
/// ActivateAction: CallbackAction(
/// onInvoke: (Intent intent) {
/// // Do something here...
/// return null;
/// },
/// ),
/// },
......@@ -818,7 +818,7 @@ class WidgetsApp extends StatefulWidget {
/// * The [Intent] and [Action] classes, which allow definition of new
/// actions.
/// {@endtemplate}
final Map<LocalKey, ActionFactory> actions;
final Map<Type, Action<Intent>> actions;
/// If true, forces the performance overlay to be visible in all instances.
///
......@@ -845,13 +845,13 @@ class WidgetsApp extends StatefulWidget {
static final Map<LogicalKeySet, Intent> _defaultShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.gameButtonA): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.gameButtonA): const ActivateIntent(),
// Keyboard traversal.
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
......@@ -869,11 +869,11 @@ class WidgetsApp extends StatefulWidget {
// Default shortcuts for the web platform.
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
// Activation
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
// Keyboard traversal.
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
// Scrolling
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
......@@ -887,12 +887,12 @@ class WidgetsApp extends StatefulWidget {
// 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),
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
// Keyboard traversal
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
......@@ -932,13 +932,13 @@ class WidgetsApp extends StatefulWidget {
}
/// 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(),
static Map<Type, Action<Intent>> defaultActions = <Type, Action<Intent>>{
DoNothingIntent: DoNothingAction(),
RequestFocusIntent: RequestFocusAction(),
NextFocusIntent: NextFocusAction(),
PreviousFocusIntent: PreviousFocusAction(),
DirectionalFocusIntent: DirectionalFocusAction(),
ScrollIntent: ScrollAction(),
};
@override
......
......@@ -92,7 +92,8 @@ enum TraversalDirection {
/// [FocusTraversalGroup] widget.
///
/// The focus traversal policy is what determines which widget is "next",
/// "previous", or in a direction from the currently focused [FocusNode].
/// "previous", or in a direction from the widget associated with the currently
/// focused [FocusNode] (usually a [Focus] widget).
///
/// One of the pre-defined subclasses may be used, or define a custom policy to
/// create a unique focus order.
......@@ -1713,88 +1714,94 @@ class _FocusTraversalGroupMarker extends InheritedWidget {
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
// A base class for all of the default actions that request focus for a node.
class _RequestFocusActionBase extends Action {
_RequestFocusActionBase(LocalKey name) : super(name);
/// An intent for use with the [RequestFocusAction], which supplies the
/// [FocusNode] that should be focused.
class RequestFocusIntent extends Intent {
/// A const constructor for a [RequestFocusIntent], so that subclasses may be
/// const.
const RequestFocusIntent(this.focusNode)
: assert(focusNode != null);
FocusNode _previousFocus;
@override
void invoke(FocusNode node, Intent intent) {
_previousFocus = primaryFocus;
node.requestFocus();
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<FocusNode>('previous', _previousFocus));
}
/// The [FocusNode] that is to be focused.
final FocusNode focusNode;
}
/// An [Action] that requests the focus on the node it is invoked on.
/// An [Action] that requests the focus on the node it is given in its
/// [RequestFocusIntent].
///
/// This action can be used to request focus for a particular node, by calling
/// [Action.invoke] like so:
///
/// ```dart
/// Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
/// Actions.invoke(context, const RequestFocusIntent(focusNode));
/// ```
///
/// Where the `_focusNode` is the node for which the focus will be requested.
/// Where the `focusNode` is the node for which the focus will be requested.
///
/// The difference between requesting focus in this way versus calling
/// [_focusNode.requestFocus] directly is that it will use the [Action]
/// registered in the nearest [Actions] widget associated with [key] to make the
/// request, rather than just requesting focus directly. This allows the action
/// to have additional side effects, like logging, or undo and redo
/// functionality.
///
/// However, this [RequestFocusAction] is the default action associated with the
/// [key] in the [WidgetsApp], and it simply requests focus and has no side
/// effects.
class RequestFocusAction extends _RequestFocusActionBase {
/// Creates a [RequestFocusAction] with a fixed [key].
RequestFocusAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to an [Intent].
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
/// [FocusNode.requestFocus] directly is that it will use the [Action]
/// registered in the nearest [Actions] widget associated with
/// [RequestFocusIntent] to make the request, rather than just requesting focus
/// directly. This allows the action to have additional side effects, like
/// logging, or undo and redo functionality.
///
/// This [RequestFocusAction] class is the default action associated with the
/// [RequestFocusIntent] in the [WidgetsApp], and it simply requests focus. You
/// can redefine the associated action with your own [Actions] widget.
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class RequestFocusAction extends Action<RequestFocusIntent> {
@override
void invoke(FocusNode node, Intent intent) => _focusAndEnsureVisible(node);
void invoke(RequestFocusIntent intent) {
_focusAndEnsureVisible(intent.focusNode);
}
}
/// An [Intent] bound to [NextFocusAction], which moves the focus to the next
/// focusable node in the focus traversal order.
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class NextFocusIntent extends Intent {
/// Creates a const [NextFocusIntent] so subclasses can be const.
const NextFocusIntent();
}
/// An [Action] that moves the focus to the next focusable node in the focus
/// order.
///
/// This action is the default action registered for the [key], and by default
/// is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
class NextFocusAction extends _RequestFocusActionBase {
/// Creates a [NextFocusAction] with a fixed [key];
NextFocusAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to an [Intent].
static const LocalKey key = ValueKey<Type>(NextFocusAction);
/// This action is the default action registered for the [NextFocusIntent], and
/// by default is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class NextFocusAction extends Action<NextFocusIntent> {
@override
void invoke(FocusNode node, Intent intent) => node.nextFocus();
void invoke(NextFocusIntent intent) {
primaryFocus.nextFocus();
}
}
/// An [Intent] bound to [PreviousFocusAction], which moves the focus to the
/// previous focusable node in the focus traversal order.
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class PreviousFocusIntent extends Intent {
/// Creates a const [PreviousFocusIntent] so subclasses can be const.
const PreviousFocusIntent();
}
/// An [Action] that moves the focus to the previous focusable node in the focus
/// order.
///
/// This action is the default action registered for the [key], and by default
/// is bound to a combination of the [LogicalKeyboardKey.tab] key and the
/// [LogicalKeyboardKey.shift] key in the [WidgetsApp].
class PreviousFocusAction extends _RequestFocusActionBase {
/// Creates a [PreviousFocusAction] with a fixed [key];
PreviousFocusAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to an [Intent].
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
/// This action is the default action registered for the [PreviousFocusIntent],
/// and by default is bound to a combination of the [LogicalKeyboardKey.tab] key
/// and the [LogicalKeyboardKey.shift] key in the [WidgetsApp].
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class PreviousFocusAction extends Action<PreviousFocusIntent> {
@override
void invoke(FocusNode node, Intent intent) => node.previousFocus();
void invoke(PreviousFocusIntent intent) {
primaryFocus.previousFocus();
}
}
/// An [Intent] that represents moving to the next focusable node in the given
......@@ -1804,12 +1811,13 @@ class PreviousFocusAction extends _RequestFocusActionBase {
/// [LogicalKeyboardKey.arrowDown], [LogicalKeyboardKey.arrowLeft], and
/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
/// appropriate associated directions.
///
/// See [FocusTraversalPolicy] for more information about focus traversal.
class DirectionalFocusIntent extends Intent {
/// Creates a [DirectionalFocusIntent] with a fixed [key], and the given
/// [direction].
/// Creates a [DirectionalFocusIntent] intending to move the focus in the
/// given [direction].
const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true})
: assert(ignoreTextFields != null),
super(DirectionalFocusAction.key);
: assert(ignoreTextFields != null);
/// The direction in which to look for the next focusable node when the
/// associated [DirectionalFocusAction] is invoked.
......@@ -1826,21 +1834,15 @@ class DirectionalFocusIntent extends Intent {
/// An [Action] that moves the focus to the focusable node in the direction
/// configured by the associated [DirectionalFocusIntent.direction].
///
/// This is the [Action] associated with the [key] and bound by default to the
/// [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
/// This is the [Action] associated with [DirectionalFocusIntent] and bound by
/// default to the [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
/// the [WidgetsApp], with the appropriate associated directions.
class DirectionalFocusAction extends _RequestFocusActionBase {
/// Creates a [DirectionalFocusAction] with a fixed [key];
DirectionalFocusAction() : super(key);
/// The [LocalKey] that uniquely identifies this action to [DirectionalFocusIntent].
static const LocalKey key = ValueKey<Type>(DirectionalFocusAction);
class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
@override
void invoke(FocusNode node, DirectionalFocusIntent intent) {
if (!intent.ignoreTextFields || node.context.widget is! EditableText) {
node.focusInDirection(intent.direction);
void invoke(DirectionalFocusIntent intent) {
if (!intent.ignoreTextFields || primaryFocus.context.widget is! EditableText) {
primaryFocus.focusInDirection(intent.direction);
}
}
}
......@@ -132,7 +132,7 @@ abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
// This map keeps track which child reserve the global key with the parent.
// This map keeps track which child reserves the global key with the parent.
// Parent, child -> global key.
// This provides us a way to remove old reservation while parent rebuilds the
// child in the same slot.
......
......@@ -889,8 +889,7 @@ class ScrollIntent extends Intent {
@required this.direction,
this.type = ScrollIncrementType.line,
}) : assert(direction != null),
assert(type != null),
super(ScrollAction.key);
assert(type != null);
/// The direction in which to scroll the scrollable containing the focused
/// widget.
......@@ -898,11 +897,6 @@ class ScrollIntent extends Intent {
/// The type of scrolling that is intended.
final ScrollIncrementType type;
@override
bool isEnabled(BuildContext context) {
return Scrollable.of(context) != null;
}
}
/// An [Action] that scrolls the [Scrollable] that encloses the current
......@@ -912,13 +906,16 @@ class ScrollIntent extends Intent {
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
/// pixels.
class ScrollAction extends Action {
/// Creates a const [ScrollAction].
ScrollAction() : super(key);
class ScrollAction extends Action<ScrollIntent> {
/// The [LocalKey] that uniquely connects this action to a [ScrollIntent].
static const LocalKey key = ValueKey<Type>(ScrollAction);
@override
bool get enabled {
final FocusNode focus = primaryFocus;
return focus != null && focus.context != null && Scrollable.of(focus.context) != null;
}
// Returns the scroll increment for a single scroll request, for use when
// scrolling using a hardware keyboard.
//
......@@ -1013,8 +1010,8 @@ class ScrollAction extends Action {
}
@override
void invoke(FocusNode node, ScrollIntent intent) {
final ScrollableState state = Scrollable.of(node.context);
void invoke(ScrollIntent intent) {
final ScrollableState state = Scrollable.of(primaryFocus.context);
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
assert(state.position.pixels != null, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
assert(state.position.viewportDimension != null);
......
......@@ -286,6 +286,14 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
/// The optional `keysPressed` argument provides an override to keys that the
/// [RawKeyboard] reports. If not specified, uses [RawKeyboard.keysPressed]
/// instead.
///
/// If a key mapping is found, then the associated action will be invoked
/// using the [Intent] that the [LogicalKeySet] maps to, and the currently
/// focused widget's context (from [FocusManager.primaryFocus]).
///
/// The object returned is the result of [Action.invoke] being called on the
/// [Action] bound to the [Intent] that the key press maps to, or null, if the
/// key press didn't match any intent.
@protected
bool handleKeypress(
BuildContext context,
......@@ -316,10 +324,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
}
if (matchedIntent != null) {
final BuildContext primaryContext = primaryFocus?.context;
if (primaryContext == null) {
return false;
}
return Actions.invoke(primaryContext, matchedIntent, nullOk: true);
assert (primaryContext != null);
Actions.invoke(primaryContext, matchedIntent, nullOk: true);
return true;
}
return false;
}
......
......@@ -174,6 +174,7 @@ void main() {
' Focus\n'
' _FocusTraversalGroupMarker\n'
' FocusTraversalGroup\n'
' _ActionsMarker\n'
' Actions\n'
' _ShortcutsMarker\n'
' Semantics\n'
......
......@@ -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 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
......@@ -259,11 +258,11 @@ void main() {
final BorderRadius borderRadius = BorderRadius.circular(6.0);
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
Future<void> buildTest(LocalKey actionKey) async {
Future<void> buildTest(Intent intent) async {
return await tester.pumpWidget(
Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.space): Intent(actionKey),
LogicalKeySet(LogicalKeyboardKey.space): intent,
},
child: Directionality(
textDirection: TextDirection.ltr,
......@@ -289,7 +288,7 @@ void main() {
);
}
await buildTest(ActivateAction.key);
await buildTest(const ActivateIntent());
focusNode.requestFocus();
await tester.pumpAndSettle();
......@@ -322,7 +321,7 @@ void main() {
);
}
await buildTest(ActivateAction.key);
await buildTest(const ActivateIntent());
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pump();
......
......@@ -323,7 +323,7 @@ void main() {
semantics.dispose();
});
testWidgets("ink response doesn't focus when disabled", (WidgetTester tester) async {
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
final GlobalKey childKey = GlobalKey();
await tester.pumpWidget(
......@@ -359,7 +359,7 @@ void main() {
});
testWidgets("ink response doesn't hover when disabled", (WidgetTester tester) async {
WidgetsBinding.instance.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTouch;
final FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
final GlobalKey childKey = GlobalKey();
bool hovering = false;
......
......@@ -47,8 +47,8 @@ void main() {
await tester.pumpWidget(
Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
},
child: Directionality(
textDirection: TextDirection.ltr,
......
......@@ -8,15 +8,19 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class TestAction extends Action {
TestAction() : super(key);
class TestIntent extends Intent {
const TestIntent();
}
class TestAction extends Action<Intent> {
TestAction();
static const LocalKey key = ValueKey<Type>(TestAction);
int calls = 0;
@override
void invoke(FocusNode node, Intent intent) {
void invoke(Intent intent) {
calls += 1;
}
}
......@@ -67,11 +71,11 @@ void main() {
await tester.pumpWidget(
WidgetsApp(
key: key,
actions: <LocalKey, ActionFactory>{
TestAction.key: () => action,
actions: <Type, Action<Intent>>{
TestIntent: action,
},
shortcuts: <LogicalKeySet, Intent> {
LogicalKeySet(LogicalKeyboardKey.space): const Intent(TestAction.key),
LogicalKeySet(LogicalKeyboardKey.space): const TestIntent(),
},
builder: (BuildContext context, Widget child) {
return Material(
......
......@@ -9,13 +9,13 @@ import 'package:flutter/src/services/keyboard_key.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
typedef PostInvokeCallback = void Function({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher});
typedef PostInvokeCallback = void Function({Action<Intent> action, Intent intent, BuildContext context, ActionDispatcher dispatcher});
class TestAction extends CallbackAction {
const TestAction({
class TestAction extends CallbackAction<TestIntent> {
TestAction({
@required OnInvokeCallback onInvoke,
}) : assert(onInvoke != null),
super(key, onInvoke: onInvoke);
super(onInvoke: onInvoke);
static const LocalKey key = ValueKey<Type>(TestAction);
}
......@@ -26,15 +26,15 @@ class TestDispatcher extends ActionDispatcher {
final PostInvokeCallback postInvoke;
@override
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
final bool result = super.invokeAction(action, intent, focusNode: focusNode);
postInvoke?.call(action: action, intent: intent, focusNode: focusNode, dispatcher: this);
Object invokeAction(Action<TestIntent> action, Intent intent, [BuildContext context]) {
final Object result = super.invokeAction(action, intent, context);
postInvoke?.call(action: action, intent: intent, context: context, dispatcher: this);
return result;
}
}
class TestIntent extends Intent {
const TestIntent() : super(TestAction.key);
const TestIntent();
}
class TestShortcutManager extends ShortcutManager {
......@@ -210,10 +210,11 @@ void main() {
bool invoked = false;
await tester.pumpWidget(
Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return true;
},
),
},
......@@ -247,10 +248,11 @@ void main() {
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
),
},
......@@ -285,10 +287,11 @@ void main() {
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
},
child: Actions(
actions: <LocalKey, ActionFactory>{
TestAction.key: () => TestAction(
onInvoke: (FocusNode node, Intent intent) {
actions: <Type, Action<Intent>>{
TestIntent: TestAction(
onInvoke: (Intent intent) {
invoked = true;
return invoked;
},
),
},
......@@ -317,7 +320,7 @@ void main() {
Shortcuts(shortcuts: <LogicalKeySet, Intent>{LogicalKeySet(
LogicalKeyboardKey.shift,
LogicalKeyboardKey.keyA,
) : const Intent(ActivateAction.key),
) : const ActivateIntent(),
LogicalKeySet(
LogicalKeyboardKey.shift,
LogicalKeyboardKey.arrowRight,
......@@ -334,7 +337,7 @@ void main() {
expect(
description[0],
equalsIgnoringHashCodes(
'shortcuts: {{Shift + Key A}: Intent#00000(key: [<ActivateAction>]), {Shift + Arrow Right}: DirectionalFocusIntent#00000(key: [<DirectionalFocusAction>])}'));
'shortcuts: {{Shift + Key A}: ActivateIntent#00000, {Shift + Arrow Right}: DirectionalFocusIntent#00000}'));
});
test('Shortcuts diagnostics work when debugLabel specified.', () {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
......@@ -345,7 +348,7 @@ void main() {
LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
): const Intent(ActivateAction.key)
): const ActivateIntent(),
},
).debugFillProperties(builder);
......@@ -368,7 +371,7 @@ void main() {
LogicalKeySet(
LogicalKeyboardKey.keyA,
LogicalKeyboardKey.keyB,
): const Intent(ActivateAction.key)
): const ActivateIntent(),
},
).debugFillProperties(builder);
......@@ -381,7 +384,7 @@ void main() {
expect(description.length, equals(2));
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: Intent#00000(key: [<ActivateAction>])}'));
expect(description[1], equalsIgnoringHashCodes('shortcuts: {{Key A + Key B}: ActivateIntent#00000}'));
});
});
}
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