Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
1ca0333c
Unverified
Commit
1ca0333c
authored
Aug 10, 2021
by
LongCatIsLooong
Committed by
GitHub
Aug 10, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Overridable action (#85743)
parent
724c0eb6
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1366 additions
and
60 deletions
+1366
-60
actions.dart
packages/flutter/lib/src/widgets/actions.dart
+599
-52
actions_test.dart
packages/flutter/test/widgets/actions_test.dart
+767
-8
No files found.
packages/flutter/lib/src/widgets/actions.dart
View file @
1ca0333c
...
@@ -72,6 +72,31 @@ typedef ActionListenerCallback = void Function(Action<Intent> action);
...
@@ -72,6 +72,31 @@ typedef ActionListenerCallback = void Function(Action<Intent> action);
/// The [ActionDispatcher] can invoke an [Action] on the primary focus, or
/// The [ActionDispatcher] can invoke an [Action] on the primary focus, or
/// without regard for focus.
/// without regard for focus.
///
///
/// ### Action Overriding
///
/// When using a leaf widget to build a more specialized widget, it's sometimes
/// desirable to change the default handling of an [Intent] defined in the leaf
/// widget. For instance, [TextField]'s [SelectAllTextIntent] by default selects
/// the text it currently contains, but in a US phone number widget that
/// consists of 3 different [TextField]s (area code, prefix and line number),
/// [SelectAllTextIntent] should instead select the text within all 3
/// [TextField]s.
///
/// An overridable [Action] is a special kind of [Action] created using the
/// [Action.overridable] constructor. It has access to a default [Action], and a
/// nullable override [Action]. It has the same behavior as its override if that
/// exists, and mirrors the behavior of its `defaultAction` otherwise.
///
/// The [Action.overridable] constructor creates overridable [Action]s that use
/// a [BuildContext] to find a suitable override in its ancestor [Actions]
/// widget. This can be used to provide a default implementation when creating a
/// general purpose leaf widget, and later override it when building a more
/// specialized widget using that leaf widget. Using the [TextField] example
/// above, the [TextField] widget uses an overridable [Action] to provide a
/// sensible default for [SelectAllTextIntent], while still allowing app
/// developers to change that if they add an ancestor [Actions] widget that maps
/// [SelectAllTextIntent] to a different [Action].
///
/// See also:
/// See also:
///
///
/// * [Shortcuts], which is a widget that contains a key map, in which it looks
/// * [Shortcuts], which is a widget that contains a key map, in which it looks
...
@@ -80,9 +105,241 @@ typedef ActionListenerCallback = void Function(Action<Intent> action);
...
@@ -80,9 +105,241 @@ typedef ActionListenerCallback = void Function(Action<Intent> action);
/// and allows redefining of actions for its descendants.
/// and allows redefining of actions for its descendants.
/// * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
/// * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
/// a given [Intent].
/// a given [Intent].
/// * [Action.overridable] for an example on how to make an [Action]
/// overridable.
abstract
class
Action
<
T
extends
Intent
>
with
Diagnosticable
{
abstract
class
Action
<
T
extends
Intent
>
with
Diagnosticable
{
/// Creates an [Action].
Action
();
/// Creates an [Action] that allows itself to be overridden by the closest
/// ancestor [Action] in the given [context] that handles the same [Intent],
/// if one exists.
///
/// When invoked, the resulting [Action] tries to find the closest [Action] in
/// the given `context` that handles the same type of [Intent] as the
/// `defaultAction`, then calls its [Action.invoke] method. When no override
/// [Action]s can be found, it invokes the `defaultAction`.
///
/// An overridable action delegates everything to its override if one exists,
/// and has the same behavior as its `defaultAction` otherwise. For this
/// reason, the override has full control over whether and how an [Intent]
/// should be handled, or a key event should be consumed. An override
/// [Action]'s [callingAction] property will be set to the [Action] it
/// currently overrides, giving it access to the default behavior. See the
/// [callingAction] property for an example.
///
/// The `context` argument is the [BuildContext] to find the override with. It
/// is typically a [BuildContext] above the [Actions] widget that contains
/// this overridable [Action].
///
/// The `defaultAction` argument is the [Action] to be invoked where there's
/// no ancestor [Action]s can't be found in `context` that handle the same
/// type of [Intent].
///
/// This is useful for providing a set of default [Action]s in a leaf widget
/// to allow further overriding, or to allow the [Intent] to propagate to
/// parent widgets that also support this [Intent].
///
/// {@tool sample --template=freeform}
/// This sample implements a custom text input field that handles the
/// [DeleteTextIntent] intent, as well as a US telephone number input widget
/// that consists of multiple text fields for area code, prefix and line
/// number. When the backspace key is pressed, the phone number input widget
/// sends the focus to the preceding text field when the currently focused
/// field becomes empty.
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// import 'package:flutter/services.dart';
/// ```
///
/// ```dart main
/// void main() {
/// runApp(
/// const MaterialApp(
/// home: Scaffold(
/// body: Center(child: SimpleUSPhoneNumberEntry()),
/// ),
/// ),
/// );
/// }
/// ```
///
/// ```dart
/// // This implements a custom phone number input field that handles the
/// // [DeleteTextIntent] intent.
/// class DigitInput extends StatefulWidget {
/// const DigitInput({
/// Key? key,
/// required this.controller,
/// required this.focusNode,
/// this.maxLength,
/// this.textInputAction = TextInputAction.next,
/// }) : super(key: key);
///
/// final int? maxLength;
/// final TextEditingController controller;
/// final TextInputAction textInputAction;
/// final FocusNode focusNode;
///
/// @override
/// DigitInputState createState() => DigitInputState();
/// }
///
/// class DigitInputState extends State<DigitInput> {
/// late final Action<DeleteTextIntent> _deleteTextAction = CallbackAction<DeleteTextIntent>(
/// onInvoke: (DeleteTextIntent intent) {
/// // For simplicity we delete everything in the section.
/// widget.controller.clear();
/// },
/// );
///
/// @override
/// Widget build(BuildContext context) {
/// return Actions(
/// actions: <Type, Action<Intent>>{
/// // Make the default `DeleteTextIntent` handler overridable.
/// DeleteTextIntent: Action<DeleteTextIntent>.overridable(defaultAction: _deleteTextAction, context: context),
/// },
/// child: TextField(
/// controller: widget.controller,
/// textInputAction: TextInputAction.next,
/// keyboardType: TextInputType.phone,
/// focusNode: widget.focusNode,
/// decoration: const InputDecoration(
/// border: OutlineInputBorder(),
/// ),
/// inputFormatters: <TextInputFormatter>[
/// FilteringTextInputFormatter.digitsOnly,
/// LengthLimitingTextInputFormatter(widget.maxLength),
/// ],
/// ),
/// );
/// }
/// }
///
/// class SimpleUSPhoneNumberEntry extends StatefulWidget {
/// const SimpleUSPhoneNumberEntry({ Key? key }) : super(key: key);
///
/// @override
/// State<SimpleUSPhoneNumberEntry> createState() => _SimpleUSPhoneNumberEntryState();
/// }
///
/// class _DeleteDigit extends Action<DeleteTextIntent> {
/// _DeleteDigit(this.state);
///
/// final _SimpleUSPhoneNumberEntryState state;
/// @override
/// Object? invoke(DeleteTextIntent intent) {
/// assert(callingAction != null);
/// callingAction?.invoke(intent);
///
/// if (state.lineNumberController.text.isEmpty && state.lineNumberFocusNode.hasFocus) {
/// state.prefixFocusNode.requestFocus();
/// }
///
/// if (state.prefixController.text.isEmpty && state.prefixFocusNode.hasFocus) {
/// state.areaCodeFocusNode.requestFocus();
/// }
/// }
///
/// // This action is only enabled when the `callingAction` exists and is
/// // enabled.
/// @override
/// bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
/// }
///
/// class _SimpleUSPhoneNumberEntryState extends State<SimpleUSPhoneNumberEntry> {
/// final FocusNode areaCodeFocusNode = FocusNode();
/// final TextEditingController areaCodeController = TextEditingController();
/// final FocusNode prefixFocusNode = FocusNode();
/// final TextEditingController prefixController = TextEditingController();
/// final FocusNode lineNumberFocusNode = FocusNode();
/// final TextEditingController lineNumberController = TextEditingController();
///
/// @override
/// Widget build(BuildContext context) {
/// return Actions(
/// actions: <Type, Action<Intent>>{
/// DeleteTextIntent : _DeleteDigit(this),
/// },
/// child: Row(
/// mainAxisAlignment: MainAxisAlignment.spaceBetween,
/// children: <Widget>[
/// const Expanded(child: Text('(', textAlign: TextAlign.center,), flex: 1),
/// Expanded(child: DigitInput(focusNode: areaCodeFocusNode, controller: areaCodeController, maxLength: 3), flex: 3),
/// const Expanded(child: Text(')', textAlign: TextAlign.center,), flex: 1),
/// Expanded(child: DigitInput(focusNode: prefixFocusNode, controller: prefixController, maxLength: 3), flex: 3),
/// const Expanded(child: Text('-', textAlign: TextAlign.center,), flex: 1),
/// Expanded(child: DigitInput(focusNode: lineNumberFocusNode, controller: lineNumberController, textInputAction: TextInputAction.done, maxLength: 4), flex: 4),
/// ],
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
factory
Action
.
overridable
({
required
Action
<
T
>
defaultAction
,
required
BuildContext
context
,
})
{
return
defaultAction
.
_makeOverridableAction
(
context
);
}
final
ObserverList
<
ActionListenerCallback
>
_listeners
=
ObserverList
<
ActionListenerCallback
>();
final
ObserverList
<
ActionListenerCallback
>
_listeners
=
ObserverList
<
ActionListenerCallback
>();
Action
<
T
>?
_currentCallingAction
;
set
_callingAction
(
Action
<
T
>?
newAction
)
{
if
(
newAction
==
_currentCallingAction
)
{
return
;
}
assert
(
newAction
==
null
||
_currentCallingAction
==
null
);
_currentCallingAction
=
newAction
;
}
/// The [Action] overridden by this [Action].
///
/// The [Action.overridable] constructor creates an overridable [Action] that
/// allows itself to be overridden by the closest ancestor [Action], and falls
/// back to its own `defaultAction` when no overrides can be found. When an
/// override is present, an overridable [Action] forwards all incoming
/// method calls to the override, and allows the override to access the
/// `defaultAction` via its [callingAction] property.
///
/// Before forwarding the call to the override, the overridable [Action] is
/// responsible for setting [callingAction] to its `defaultAction`, which is
/// already taken care of by the overridable [Action] created using
/// [Action.overridable].
///
/// This property is only non-null when this [Action] is an override of the
/// [callingAction], and is currently being invoked from [callingAction].
///
/// Invoking [callingAction]'s methods, or accessing its properties, is
/// allowed and does not introduce infinite loops or infinite recursions.
///
/// {@tool snippet}
/// An example `Action` that handles [PasteTextIntent] but has mostly the same
/// behavior as the overridable action. It's OK to call
/// `callingAction?.isActionEnabled` in the implementation of this `Action`.
///
/// ```dart
/// class MyPasteAction extends Action<PasteTextIntent> {
/// @override
/// Object? invoke(PasteTextIntent intent) {
/// print(intent);
/// return callingAction?.invoke(intent);
/// }
///
/// @override
/// bool get isActionEnabled => callingAction?.isActionEnabled ?? false;
///
/// @override
/// bool consumesKey(PasteTextIntent intent) => callingAction?.consumesKey(intent) ?? false;
/// }
/// ```
/// {@end-tool}
@protected
Action
<
T
>?
get
callingAction
=>
_currentCallingAction
;
/// Gets the type of intent this action responds to.
/// Gets the type of intent this action responds to.
Type
get
intentType
=>
T
;
Type
get
intentType
=>
T
;
...
@@ -90,10 +347,19 @@ abstract class Action<T extends Intent> with Diagnosticable {
...
@@ -90,10 +347,19 @@ abstract class Action<T extends Intent> with Diagnosticable {
///
///
/// This will be called by the [ActionDispatcher] before attempting to invoke
/// This will be called by the [ActionDispatcher] before attempting to invoke
/// the action.
/// the action.
bool
isEnabled
(
T
intent
)
=>
isActionEnabled
;
/// Whether this [Action] is inherently enabled.
///
///
/// If [isActionEnabled] is false, then this [Action] is disabled for any
/// given [Intent].
//
/// If the enabled state changes, overriding subclasses must call
/// If the enabled state changes, overriding subclasses must call
/// [notifyActionListeners] to notify any listeners of the change.
/// [notifyActionListeners] to notify any listeners of the change.
bool
isEnabled
(
covariant
T
intent
)
=>
true
;
///
/// In the case of an overridable `Action`, accessing this property creates
/// an dependency on the overridable `Action`s `lookupContext`.
bool
get
isActionEnabled
=>
true
;
/// Indicates whether this action should treat key events mapped to this
/// Indicates whether this action should treat key events mapped to this
/// action as being "handled" when it is invoked via the key event.
/// action as being "handled" when it is invoked via the key event.
...
@@ -106,7 +372,7 @@ abstract class Action<T extends Intent> with Diagnosticable {
...
@@ -106,7 +372,7 @@ abstract class Action<T extends Intent> with Diagnosticable {
/// widgets to receive the key event.
/// widgets to receive the key event.
///
///
/// The default implementation returns true.
/// The default implementation returns true.
bool
consumesKey
(
covariant
T
intent
)
=>
true
;
bool
consumesKey
(
T
intent
)
=>
true
;
/// Called when the action is to be performed.
/// Called when the action is to be performed.
///
///
...
@@ -143,7 +409,7 @@ abstract class Action<T extends Intent> with Diagnosticable {
...
@@ -143,7 +409,7 @@ abstract class Action<T extends Intent> with Diagnosticable {
/// [Actions.invoke], or by invoking it using an [ActionDispatcher]. An action
/// [Actions.invoke], or by invoking it using an [ActionDispatcher]. An action
/// invoked via a [Shortcuts] widget will have its return value ignored.
/// invoked via a [Shortcuts] widget will have its return value ignored.
@protected
@protected
Object
?
invoke
(
covariant
T
intent
);
Object
?
invoke
(
T
intent
);
/// Register a callback to listen for changes to the state of this action.
/// Register a callback to listen for changes to the state of this action.
///
///
...
@@ -234,6 +500,10 @@ abstract class Action<T extends Intent> with Diagnosticable {
...
@@ -234,6 +500,10 @@ abstract class Action<T extends Intent> with Diagnosticable {
}
}
}
}
}
}
Action
<
T
>
_makeOverridableAction
(
BuildContext
context
)
{
return
_OverridableAction
<
T
>(
defaultAction:
this
,
lookupContext:
context
);
}
}
}
/// A helper widget for making sure that listeners on an action are removed properly.
/// A helper widget for making sure that listeners on an action are removed properly.
...
@@ -447,7 +717,12 @@ abstract class ContextAction<T extends Intent> extends Action<T> {
...
@@ -447,7 +717,12 @@ abstract class ContextAction<T extends Intent> extends Action<T> {
/// ```
/// ```
@protected
@protected
@override
@override
Object
?
invoke
(
covariant
T
intent
,
[
BuildContext
?
context
]);
Object
?
invoke
(
T
intent
,
[
BuildContext
?
context
]);
@override
ContextAction
<
T
>
_makeOverridableAction
(
BuildContext
context
)
{
return
_OverridableContextAction
<
T
>(
defaultAction:
this
,
lookupContext:
context
);
}
}
}
/// The signature of a callback accepted by [CallbackAction].
/// The signature of a callback accepted by [CallbackAction].
...
@@ -478,7 +753,7 @@ class CallbackAction<T extends Intent> extends Action<T> {
...
@@ -478,7 +753,7 @@ class CallbackAction<T extends Intent> extends Action<T> {
final
OnInvokeCallback
<
T
>
onInvoke
;
final
OnInvokeCallback
<
T
>
onInvoke
;
@override
@override
Object
?
invoke
(
covariant
T
intent
)
=>
onInvoke
(
intent
);
Object
?
invoke
(
T
intent
)
=>
onInvoke
(
intent
);
}
}
/// An action dispatcher that simply invokes the actions given to it.
/// An action dispatcher that simply invokes the actions given to it.
...
@@ -865,7 +1140,7 @@ class Actions extends StatefulWidget {
...
@@ -865,7 +1140,7 @@ class Actions extends StatefulWidget {
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
Action
<
T
>?
result
=
actions
.
actions
[
type
]
as
Action
<
T
>?
;
final
Action
<
T
>?
result
=
_castAction
(
actions
,
intent:
intent
)
;
if
(
result
!=
null
)
{
if
(
result
!=
null
)
{
context
.
dependOnInheritedElement
(
element
);
context
.
dependOnInheritedElement
(
element
);
action
=
result
;
action
=
result
;
...
@@ -877,6 +1152,49 @@ class Actions extends StatefulWidget {
...
@@ -877,6 +1152,49 @@ class Actions extends StatefulWidget {
return
action
;
return
action
;
}
}
static
Action
<
T
>?
_maybeFindWithoutDependingOn
<
T
extends
Intent
>(
BuildContext
context
,
{
T
?
intent
})
{
Action
<
T
>?
action
;
// Specialize the type if a runtime example instance of the intent is given.
// This allows this function to be called by code that doesn't know the
// concrete type of the intent at compile time.
final
Type
type
=
intent
?.
runtimeType
??
T
;
assert
(
type
!=
Intent
,
'The type passed to "find" resolved to "Intent": either a non-Intent '
'generic type argument or an example intent derived from Intent must be '
'specified. Intent may be used as the generic type as long as the optional '
'"intent" argument is passed.'
,
);
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
Action
<
T
>?
result
=
_castAction
(
actions
,
intent:
intent
);
if
(
result
!=
null
)
{
action
=
result
;
return
true
;
}
return
false
;
});
return
action
;
}
// Find the [Action] that handles the given `intent` in the given
// `_ActionsMarker`, and verify it has the right type parameter.
static
Action
<
T
>?
_castAction
<
T
extends
Intent
>(
_ActionsMarker
actionsMarker
,
{
T
?
intent
})
{
final
Action
<
Intent
>?
mappedAction
=
actionsMarker
.
actions
[
intent
?.
runtimeType
??
T
];
if
(
mappedAction
is
Action
<
T
>?)
{
return
mappedAction
;
}
else
{
assert
(
false
,
'
$T
cannot be handled by an Action of runtime type
${mappedAction.runtimeType}
.'
);
return
null
;
}
}
/// Returns the [ActionDispatcher] associated with the [Actions] widget that
/// Returns the [ActionDispatcher] associated with the [Actions] widget that
/// most tightly encloses the given [BuildContext].
/// most tightly encloses the given [BuildContext].
///
///
...
@@ -896,38 +1214,33 @@ class Actions extends StatefulWidget {
...
@@ -896,38 +1214,33 @@ class Actions extends StatefulWidget {
///
///
/// The `context` and `intent` arguments must not be null.
/// The `context` and `intent` arguments must not be null.
///
///
/// If the given `intent` doesn't map to an action, or doesn't map to one that
/// If the given `intent` doesn't map to an action, then it will look to the
/// returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
/// next ancestor [Actions] widget in the hierarchy until it reaches the root.
/// then it will look to the next ancestor [Actions] widget in the hierarchy
/// until it reaches the root.
///
///
/// This method will throw an exception if no ambient [Actions] widget is
/// This method will throw an exception if no ambient [Actions] widget is
/// found, or
if the given `intent` doesn't map to an enabled action in any of
/// found, or
when a suitable [Action] is found but it returns false for
///
the [Actions.actions] maps that are found
.
///
[Action.isEnabled]
.
static
Object
?
invoke
<
T
extends
Intent
>(
static
Object
?
invoke
<
T
extends
Intent
>(
BuildContext
context
,
BuildContext
context
,
T
intent
,
T
intent
,
)
{
)
{
assert
(
intent
!=
null
);
assert
(
intent
!=
null
);
assert
(
context
!=
null
);
assert
(
context
!=
null
);
Action
<
T
>?
action
;
Object
?
returnValue
;
InheritedElement
?
actionElement
;
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
final
bool
actionFound
=
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
Action
<
T
>?
result
=
actions
.
actions
[
intent
.
runtimeType
]
as
Action
<
T
>?;
final
Action
<
T
>?
result
=
_castAction
(
actions
,
intent:
intent
);
if
(
result
!=
null
)
{
if
(
result
!=
null
&&
result
.
isEnabled
(
intent
))
{
actionElement
=
element
;
// Invoke the action we found using the relevant dispatcher from the Actions
if
(
result
.
isEnabled
(
intent
))
{
// Element we found.
action
=
result
;
returnValue
=
_findDispatcher
(
element
).
invokeAction
(
result
,
intent
,
context
);
return
true
;
}
}
}
return
false
;
return
result
!=
null
;
});
});
assert
(()
{
assert
(()
{
if
(
actionElement
==
null
)
{
if
(
!
actionFound
)
{
throw
FlutterError
(
throw
FlutterError
(
'Unable to find an action for an Intent with type '
'Unable to find an action for an Intent with type '
'
${intent.runtimeType}
in an
$Actions
widget in the given context.
\n
'
'
${intent.runtimeType}
in an
$Actions
widget in the given context.
\n
'
...
@@ -943,12 +1256,7 @@ class Actions extends StatefulWidget {
...
@@ -943,12 +1256,7 @@ class Actions extends StatefulWidget {
}
}
return
true
;
return
true
;
}());
}());
if
(
actionElement
==
null
||
action
==
null
)
{
return
returnValue
;
return
null
;
}
// Invoke the action we found using the relevant dispatcher from the Actions
// Element we found.
return
_findDispatcher
(
actionElement
!).
invokeAction
(
action
!,
intent
,
context
);
}
}
/// Invokes the action associated with the given [Intent] using the
/// Invokes the action associated with the given [Intent] using the
...
@@ -956,43 +1264,34 @@ class Actions extends StatefulWidget {
...
@@ -956,43 +1264,34 @@ class Actions extends StatefulWidget {
///
///
/// This method returns the result of invoking the action's [Action.invoke]
/// This method returns the result of invoking the action's [Action.invoke]
/// method. If no action mapping was found for the specified intent, or if the
/// method. If no action mapping was found for the specified intent, or if the
///
actions that were found were
disabled, or the action itself returns null
///
first action found was
disabled, or the action itself returns null
/// from [Action.invoke], then this method returns null.
/// from [Action.invoke], then this method returns null.
///
///
/// The `context` and `intent` arguments must not be null.
/// The `context` and `intent` arguments must not be null.
///
///
/// If the given `intent` doesn't map to an action,
or doesn't map to one that
/// If the given `intent` doesn't map to an action,
then it will look to the
///
returns true for [Action.isEnabled] in an [Actions.actions] map it finds,
///
next ancestor [Actions] widget in the hierarchy until it reaches the root.
///
then it will look to the next ancestor [Actions] widget in the hierarchy
///
If a suitable [Action] is found but its [Action.isEnabled] returns false,
///
until it reaches the root
.
///
the search will stop and this method will return null
.
static
Object
?
maybeInvoke
<
T
extends
Intent
>(
static
Object
?
maybeInvoke
<
T
extends
Intent
>(
BuildContext
context
,
BuildContext
context
,
T
intent
,
T
intent
,
)
{
)
{
assert
(
intent
!=
null
);
assert
(
intent
!=
null
);
assert
(
context
!=
null
);
assert
(
context
!=
null
);
Action
<
T
>?
action
;
Object
?
returnValue
;
InheritedElement
?
actionElement
;
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
_visitActionsAncestors
(
context
,
(
InheritedElement
element
)
{
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
_ActionsMarker
actions
=
element
.
widget
as
_ActionsMarker
;
final
Action
<
T
>?
result
=
actions
.
actions
[
intent
.
runtimeType
]
as
Action
<
T
>?;
final
Action
<
T
>?
result
=
_castAction
(
actions
,
intent:
intent
);
if
(
result
!=
null
)
{
if
(
result
!=
null
&&
result
.
isEnabled
(
intent
))
{
actionElement
=
element
;
// Invoke the action we found using the relevant dispatcher from the Actions
if
(
result
.
isEnabled
(
intent
))
{
// Element we found.
action
=
result
;
returnValue
=
_findDispatcher
(
element
).
invokeAction
(
result
,
intent
,
context
);
return
true
;
}
}
}
return
false
;
return
result
!=
null
;
});
});
return
returnValue
;
if
(
actionElement
==
null
||
action
==
null
)
{
return
null
;
}
// Invoke the action we found using the relevant dispatcher from the Actions
// Element we found.
return
_findDispatcher
(
actionElement
!).
invokeAction
(
action
!,
intent
,
context
);
}
}
@override
@override
...
@@ -1682,3 +1981,251 @@ class PrioritizedAction extends Action<PrioritizedIntents> {
...
@@ -1682,3 +1981,251 @@ class PrioritizedAction extends Action<PrioritizedIntents> {
_selectedAction
.
invoke
(
_selectedIntent
);
_selectedAction
.
invoke
(
_selectedIntent
);
}
}
}
}
mixin
_OverridableActionMixin
<
T
extends
Intent
>
on
Action
<
T
>
{
// When debugAssertMutuallyRecursive is true, this action will throw an
// assertion error when the override calls this action's "invoke" method and
// the override is already being invoked from within the "invoke" method.
bool
debugAssertMutuallyRecursive
=
false
;
bool
debugAssertIsActionEnabledMutuallyRecursive
=
false
;
bool
debugAssertIsEnabledMutuallyRecursive
=
false
;
bool
debugAssertConsumeKeyMutuallyRecursive
=
false
;
// The default action to invoke if an enabled override Action can't be found
// using [lookupContext];
Action
<
T
>
get
defaultAction
;
// The [BuildContext] used to find the override of this [Action].
BuildContext
get
lookupContext
;
// How to invoke [defaultAction], given the caller [fromAction].
Object
?
invokeDefaultAction
(
T
intent
,
Action
<
T
>?
fromAction
,
BuildContext
?
context
);
Action
<
T
>?
getOverrideAction
({
bool
declareDependency
=
false
})
{
final
Action
<
T
>?
override
=
declareDependency
?
Actions
.
maybeFind
(
lookupContext
)
:
Actions
.
_maybeFindWithoutDependingOn
(
lookupContext
);
assert
(!
identical
(
override
,
this
));
return
override
;
}
@override
set
_callingAction
(
Action
<
T
>?
newAction
)
{
super
.
_callingAction
=
newAction
;
defaultAction
.
_callingAction
=
newAction
;
}
Object
?
_invokeOverride
(
Action
<
T
>
overrideAction
,
T
intent
,
BuildContext
?
context
)
{
assert
(!
debugAssertMutuallyRecursive
);
assert
(()
{
debugAssertMutuallyRecursive
=
true
;
return
true
;
}());
overrideAction
.
_callingAction
=
defaultAction
;
final
Object
?
returnValue
=
overrideAction
is
ContextAction
<
T
>
?
overrideAction
.
invoke
(
intent
,
context
)
:
overrideAction
.
invoke
(
intent
);
overrideAction
.
_callingAction
=
null
;
assert
(()
{
debugAssertMutuallyRecursive
=
false
;
return
true
;
}());
return
returnValue
;
}
@override
Object
?
invoke
(
T
intent
,
[
BuildContext
?
context
])
{
final
Action
<
T
>?
overrideAction
=
getOverrideAction
();
final
Object
?
returnValue
=
overrideAction
==
null
?
invokeDefaultAction
(
intent
,
callingAction
,
context
)
:
_invokeOverride
(
overrideAction
,
intent
,
context
);
return
returnValue
;
}
bool
isOverrideActionEnabled
(
Action
<
T
>
overrideAction
)
{
assert
(!
debugAssertIsActionEnabledMutuallyRecursive
);
assert
(()
{
debugAssertIsActionEnabledMutuallyRecursive
=
true
;
return
true
;
}());
overrideAction
.
_callingAction
=
defaultAction
;
final
bool
isOverrideEnabled
=
overrideAction
.
isActionEnabled
;
overrideAction
.
_callingAction
=
null
;
assert
(()
{
debugAssertIsActionEnabledMutuallyRecursive
=
false
;
return
true
;
}());
return
isOverrideEnabled
;
}
@override
bool
get
isActionEnabled
{
final
Action
<
T
>?
overrideAction
=
getOverrideAction
(
declareDependency:
true
);
final
bool
returnValue
=
overrideAction
!=
null
?
isOverrideActionEnabled
(
overrideAction
)
:
defaultAction
.
isActionEnabled
;
return
returnValue
;
}
@override
bool
isEnabled
(
T
intent
)
{
assert
(!
debugAssertIsEnabledMutuallyRecursive
);
assert
(()
{
debugAssertIsEnabledMutuallyRecursive
=
true
;
return
true
;
}());
final
Action
<
T
>?
overrideAction
=
getOverrideAction
();
overrideAction
?.
_callingAction
=
defaultAction
;
final
bool
returnValue
=
(
overrideAction
??
defaultAction
).
isEnabled
(
intent
);
overrideAction
?.
_callingAction
=
null
;
assert
(()
{
debugAssertIsEnabledMutuallyRecursive
=
false
;
return
true
;
}());
return
returnValue
;
}
@override
bool
consumesKey
(
T
intent
)
{
assert
(!
debugAssertConsumeKeyMutuallyRecursive
);
assert
(()
{
debugAssertConsumeKeyMutuallyRecursive
=
true
;
return
true
;
}());
final
Action
<
T
>?
overrideAction
=
getOverrideAction
();
overrideAction
?.
_callingAction
=
defaultAction
;
final
bool
isEnabled
=
(
overrideAction
??
defaultAction
).
consumesKey
(
intent
);
overrideAction
?.
_callingAction
=
null
;
assert
(()
{
debugAssertConsumeKeyMutuallyRecursive
=
false
;
return
true
;
}());
return
isEnabled
;
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
Action
<
T
>>(
'defaultAction'
,
defaultAction
));
}
}
class
_OverridableAction
<
T
extends
Intent
>
extends
ContextAction
<
T
>
with
_OverridableActionMixin
<
T
>
{
_OverridableAction
({
required
this
.
defaultAction
,
required
this
.
lookupContext
})
;
@override
final
Action
<
T
>
defaultAction
;
@override
final
BuildContext
lookupContext
;
@override
Object
?
invokeDefaultAction
(
T
intent
,
Action
<
T
>?
fromAction
,
BuildContext
?
context
)
{
if
(
fromAction
==
null
)
{
return
defaultAction
.
invoke
(
intent
);
}
else
{
final
Object
?
returnValue
=
defaultAction
.
invoke
(
intent
);
return
returnValue
;
}
}
@override
ContextAction
<
T
>
_makeOverridableAction
(
BuildContext
context
)
{
return
_OverridableAction
<
T
>(
defaultAction:
defaultAction
,
lookupContext:
context
);
}
}
class
_OverridableContextAction
<
T
extends
Intent
>
extends
ContextAction
<
T
>
with
_OverridableActionMixin
<
T
>
{
_OverridableContextAction
({
required
this
.
defaultAction
,
required
this
.
lookupContext
});
@override
final
ContextAction
<
T
>
defaultAction
;
@override
final
BuildContext
lookupContext
;
@override
Object
?
_invokeOverride
(
Action
<
T
>
overrideAction
,
T
intent
,
BuildContext
?
context
)
{
assert
(
context
!=
null
);
assert
(!
debugAssertMutuallyRecursive
);
assert
(()
{
debugAssertMutuallyRecursive
=
true
;
return
true
;
}());
// Wrap the default Action together with the calling context in case
// overrideAction is not a ContextAction and thus have no access to the
// calling BuildContext.
final
Action
<
T
>
wrappedDefault
=
_ContextActionToActionAdapter
<
T
>(
invokeContext:
context
!,
action:
defaultAction
);
overrideAction
.
_callingAction
=
wrappedDefault
;
final
Object
?
returnValue
=
overrideAction
is
ContextAction
<
T
>
?
overrideAction
.
invoke
(
intent
,
context
)
:
overrideAction
.
invoke
(
intent
);
overrideAction
.
_callingAction
=
null
;
assert
(()
{
debugAssertMutuallyRecursive
=
false
;
return
true
;
}());
return
returnValue
;
}
@override
Object
?
invokeDefaultAction
(
T
intent
,
Action
<
T
>?
fromAction
,
BuildContext
?
context
)
{
if
(
fromAction
==
null
)
{
return
defaultAction
.
invoke
(
intent
,
context
);
}
else
{
final
Object
?
returnValue
=
defaultAction
.
invoke
(
intent
,
context
);
return
returnValue
;
}
}
@override
ContextAction
<
T
>
_makeOverridableAction
(
BuildContext
context
)
{
return
_OverridableContextAction
<
T
>(
defaultAction:
defaultAction
,
lookupContext:
context
);
}
}
class
_ContextActionToActionAdapter
<
T
extends
Intent
>
extends
Action
<
T
>
{
_ContextActionToActionAdapter
({
required
this
.
invokeContext
,
required
this
.
action
});
final
BuildContext
invokeContext
;
final
ContextAction
<
T
>
action
;
@override
set
_callingAction
(
Action
<
T
>?
newAction
)
{
action
.
_callingAction
=
newAction
;
}
@override
Action
<
T
>?
get
callingAction
=>
action
.
callingAction
;
@override
bool
isEnabled
(
T
intent
)
=>
action
.
isEnabled
(
intent
);
@override
bool
get
isActionEnabled
=>
action
.
isActionEnabled
;
@override
bool
consumesKey
(
T
intent
)
=>
action
.
consumesKey
(
intent
);
@override
void
addActionListener
(
ActionListenerCallback
listener
)
{
super
.
addActionListener
(
listener
);
action
.
addActionListener
(
listener
);
}
@override
void
removeActionListener
(
ActionListenerCallback
listener
)
{
super
.
removeActionListener
(
listener
);
action
.
removeActionListener
(
listener
);
}
@override
@protected
void
notifyActionListeners
()
=>
action
.
notifyActionListeners
();
@override
Object
?
invoke
(
T
intent
)
=>
action
.
invoke
(
intent
,
invokeContext
);
}
packages/flutter/test/widgets/actions_test.dart
View file @
1ca0333c
...
@@ -549,7 +549,7 @@ void main() {
...
@@ -549,7 +549,7 @@ void main() {
expect
(
invokedDispatcher
.
runtimeType
,
equals
(
TestDispatcher1
));
expect
(
invokedDispatcher
.
runtimeType
,
equals
(
TestDispatcher1
));
expect
(
testAction
.
capturedContexts
.
single
,
containerKey
.
currentContext
);
expect
(
testAction
.
capturedContexts
.
single
,
containerKey
.
currentContext
);
});
});
testWidgets
(
'Disabled actions
allow
propagation to an ancestor'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Disabled actions
stop
propagation to an ancestor'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
containerKey
=
GlobalKey
();
final
GlobalKey
containerKey
=
GlobalKey
();
bool
invoked
=
false
;
bool
invoked
=
false
;
const
TestIntent
intent
=
TestIntent
();
const
TestIntent
intent
=
TestIntent
();
...
@@ -589,11 +589,11 @@ void main() {
...
@@ -589,11 +589,11 @@ void main() {
containerKey
.
currentContext
!,
containerKey
.
currentContext
!,
intent
,
intent
,
);
);
expect
(
result
,
is
True
);
expect
(
result
,
is
Null
);
expect
(
invoked
,
is
Tru
e
);
expect
(
invoked
,
is
Fals
e
);
expect
(
invokedIntent
,
equals
(
intent
)
);
expect
(
invokedIntent
,
isNull
);
expect
(
invokedAction
,
equals
(
enabledTestAction
)
);
expect
(
invokedAction
,
isNull
);
expect
(
invokedDispatcher
.
runtimeType
,
equals
(
TestDispatcher1
)
);
expect
(
invokedDispatcher
,
isNull
);
});
});
});
});
...
@@ -651,7 +651,7 @@ void main() {
...
@@ -651,7 +651,7 @@ void main() {
),
),
);
);
Object
?
result
=
Actions
.
i
nvoke
(
Object
?
result
=
Actions
.
maybeI
nvoke
(
containerKey
.
currentContext
!,
containerKey
.
currentContext
!,
const
TestIntent
(),
const
TestIntent
(),
);
);
...
@@ -689,7 +689,7 @@ void main() {
...
@@ -689,7 +689,7 @@ void main() {
);
);
await
tester
.
pump
();
await
tester
.
pump
();
result
=
Actions
.
i
nvoke
<
TestIntent
>(
result
=
Actions
.
maybeI
nvoke
<
TestIntent
>(
containerKey
.
currentContext
!,
containerKey
.
currentContext
!,
const
SecondTestIntent
(),
const
SecondTestIntent
(),
);
);
...
@@ -1025,6 +1025,677 @@ void main() {
...
@@ -1025,6 +1025,677 @@ void main() {
expect
(
description
[
1
],
equalsIgnoringHashCodes
(
'actions: {TestIntent: TestAction#00000}'
));
expect
(
description
[
1
],
equalsIgnoringHashCodes
(
'actions: {TestIntent: TestAction#00000}'
));
});
});
});
});
group
(
'Action overriding'
,
()
{
final
List
<
String
>
invocations
=
<
String
>[];
BuildContext
?
invokingContext
;
tearDown
(()
{
invocations
.
clear
();
invokingContext
=
null
;
});
testWidgets
(
'Basic usage'
,
(
WidgetTester
tester
)
async
{
late
BuildContext
invokingContext2
;
late
BuildContext
invokingContext3
;
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent
:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
invokingContext2
=
context2
;
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent
:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
invokingContext3
=
context3
;
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
invocations
.
clear
();
// Invoke from a different (higher) context.
Actions
.
invoke
(
invokingContext3
,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invoke'
,
'action1.invokeAsOverride-post-super'
,
]);
invocations
.
clear
();
// Invoke from a different (higher) context.
Actions
.
invoke
(
invokingContext2
,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invoke'
]);
});
testWidgets
(
'Does not break after use'
,
(
WidgetTester
tester
)
async
{
late
BuildContext
invokingContext2
;
late
BuildContext
invokingContext3
;
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
invokingContext2
=
context2
;
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
invokingContext3
=
context3
;
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
// Invoke a bunch of times and verify it still produces the same result.
final
List
<
BuildContext
>
randomContexts
=
<
BuildContext
>[
invokingContext
!,
invokingContext2
,
invokingContext
!,
invokingContext3
,
invokingContext3
,
invokingContext3
,
invokingContext2
,
];
for
(
final
BuildContext
randomContext
in
randomContexts
)
{
Actions
.
invoke
(
randomContext
,
LogIntent
(
log:
invocations
));
}
invocations
.
clear
();
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
});
testWidgets
(
'Does not override if not overridable'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent
:
LogInvocationAction
(
actionName:
'action2'
)
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
]);
});
testWidgets
(
'The final override controls isEnabled'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
,
enabled:
false
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
invocations
.
clear
();
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
,
enabled:
false
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[]);
});
testWidgets
(
'The override can choose to defer isActionEnabled to the overridable'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationButDeferIsEnabledAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
,
enabled:
false
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
// Nothing since the final override defers its isActionEnabled state to action2,
// which is disabled.
expect
(
invocations
,
<
String
>[]);
invocations
.
clear
();
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
,
enabled:
true
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationButDeferIsEnabledAction
(
actionName:
'action2'
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
,
enabled:
false
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
// The final override (action1) is enabled so all 3 actions are enabled.
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
});
testWidgets
(
'Throws on infinite recursions'
,
(
WidgetTester
tester
)
async
{
late
StateSetter
setState
;
BuildContext
?
action2LookupContext
;
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
StatefulBuilder
(
builder:
(
BuildContext
context2
,
StateSetter
stateSetter
)
{
setState
=
stateSetter
;
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
if
(
action2LookupContext
!=
null
)
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
),
context:
action2LookupContext
!)
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
// Let action2 look up its override using a context below itself, so it
// will find action3 as its override.
expect
(
tester
.
takeException
(),
isNull
);
setState
(()
{
action2LookupContext
=
invokingContext
;
});
await
tester
.
pump
();
expect
(
tester
.
takeException
(),
isNull
);
Object
?
exception
;
try
{
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
}
catch
(
e
)
{
exception
=
e
;
}
expect
(
exception
?.
toString
(),
contains
(
'debugAssertIsEnabledMutuallyRecursive'
));
});
testWidgets
(
'Throws on invoking invalid override'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent
:
TestContextAction
()
},
child:
Builder
(
builder:
(
BuildContext
context
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context
),
},
child:
Builder
(
builder:
(
BuildContext
context1
)
{
invokingContext
=
context1
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
Object
?
exception
;
try
{
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
}
catch
(
e
)
{
exception
=
e
;
}
expect
(
exception
?.
toString
(),
contains
(
'cannot be handled by an Action of runtime type TestContextAction.'
),
);
});
testWidgets
(
'Make an overridable action overridable'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
Action
<
LogIntent
>.
overridable
(
defaultAction:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context1
,
),
context:
context2
,
),
context:
context3
,
)
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
});
testWidgets
(
'Overriding Actions can change the intent'
,
(
WidgetTester
tester
)
async
{
final
List
<
String
>
newLogChannel
=
<
String
>[];
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
RedirectOutputAction
(
actionName:
'action2'
,
newLog:
newLogChannel
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action1.invokeAsOverride-post-super'
,
]);
expect
(
newLogChannel
,
<
String
>[
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
]);
});
testWidgets
(
'Override non-context overridable Actions with a ContextAction'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
// The default Action is a ContextAction subclass.
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationContextAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action2'
,
enabled:
false
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
// Action1 is a ContextAction and action2 & action3 are not.
// They should not lose information.
expect
(
LogInvocationContextAction
.
invokeContext
,
isNotNull
);
expect
(
LogInvocationContextAction
.
invokeContext
,
invokingContext
);
});
testWidgets
(
'Override a ContextAction with a regular Action'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Builder
(
builder:
(
BuildContext
context1
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action1'
),
context:
context1
),
},
child:
Builder
(
builder:
(
BuildContext
context2
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationContextAction
(
actionName:
'action2'
,
enabled:
false
),
context:
context2
),
},
child:
Builder
(
builder:
(
BuildContext
context3
)
{
return
Actions
(
actions:
<
Type
,
Action
<
Intent
>>
{
LogIntent:
Action
<
LogIntent
>.
overridable
(
defaultAction:
LogInvocationAction
(
actionName:
'action3'
),
context:
context3
),
},
child:
Builder
(
builder:
(
BuildContext
context4
)
{
invokingContext
=
context4
;
return
const
SizedBox
();
},
),
);
},
),
);
},
),
);
},
),
);
Actions
.
invoke
(
invokingContext
!,
LogIntent
(
log:
invocations
));
expect
(
invocations
,
<
String
>[
'action1.invokeAsOverride-pre-super'
,
'action2.invokeAsOverride-pre-super'
,
'action3.invoke'
,
'action2.invokeAsOverride-post-super'
,
'action1.invokeAsOverride-post-super'
,
]);
// Action2 is a ContextAction and action1 & action2 are regular actions.
// Invoking action2 from action3 should still supply a non-null
// BuildContext.
expect
(
LogInvocationContextAction
.
invokeContext
,
isNotNull
);
expect
(
LogInvocationContextAction
.
invokeContext
,
invokingContext
);
});
});
}
}
class
TestContextAction
extends
ContextAction
<
TestIntent
>
{
class
TestContextAction
extends
ContextAction
<
TestIntent
>
{
...
@@ -1036,3 +1707,91 @@ class TestContextAction extends ContextAction<TestIntent> {
...
@@ -1036,3 +1707,91 @@ class TestContextAction extends ContextAction<TestIntent> {
return
null
;
return
null
;
}
}
}
}
class
LogIntent
extends
Intent
{
const
LogIntent
({
required
this
.
log
});
final
List
<
String
>
log
;
}
class
LogInvocationAction
extends
Action
<
LogIntent
>
{
LogInvocationAction
({
required
this
.
actionName
,
this
.
enabled
=
true
});
final
String
actionName
;
final
bool
enabled
;
@override
bool
get
isActionEnabled
=>
enabled
;
@override
Object
?
invoke
(
LogIntent
intent
)
{
final
Action
<
LogIntent
>?
callingAction
=
this
.
callingAction
;
if
(
callingAction
==
null
)
{
intent
.
log
.
add
(
'
$actionName
.invoke'
);
}
else
{
intent
.
log
.
add
(
'
$actionName
.invokeAsOverride-pre-super'
);
callingAction
.
invoke
(
intent
);
intent
.
log
.
add
(
'
$actionName
.invokeAsOverride-post-super'
);
}
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
StringProperty
(
'actionName'
,
actionName
));
}
}
class
LogInvocationContextAction
extends
ContextAction
<
LogIntent
>
{
LogInvocationContextAction
({
required
this
.
actionName
,
this
.
enabled
=
true
});
static
BuildContext
?
invokeContext
;
final
String
actionName
;
final
bool
enabled
;
@override
bool
get
isActionEnabled
=>
enabled
;
@override
Object
?
invoke
(
LogIntent
intent
,
[
BuildContext
?
context
])
{
invokeContext
=
context
;
final
Action
<
LogIntent
>?
callingAction
=
this
.
callingAction
;
if
(
callingAction
==
null
)
{
intent
.
log
.
add
(
'
$actionName
.invoke'
);
}
else
{
intent
.
log
.
add
(
'
$actionName
.invokeAsOverride-pre-super'
);
callingAction
.
invoke
(
intent
);
intent
.
log
.
add
(
'
$actionName
.invokeAsOverride-post-super'
);
}
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
StringProperty
(
'actionName'
,
actionName
));
}
}
class
LogInvocationButDeferIsEnabledAction
extends
LogInvocationAction
{
LogInvocationButDeferIsEnabledAction
({
required
String
actionName
})
:
super
(
actionName:
actionName
);
// Defer `isActionEnabled` to the overridable action.
@override
bool
get
isActionEnabled
=>
callingAction
?.
isActionEnabled
??
false
;
}
class
RedirectOutputAction
extends
LogInvocationAction
{
RedirectOutputAction
({
required
String
actionName
,
bool
enabled
=
true
,
required
this
.
newLog
,
})
:
super
(
actionName:
actionName
,
enabled:
enabled
);
final
List
<
String
>
newLog
;
@override
Object
?
invoke
(
LogIntent
intent
)
=>
super
.
invoke
(
LogIntent
(
log:
newLog
));
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment