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
This commit is contained in:
parent
c663cd55a7
commit
0f68b46f6a
@ -2,6 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -14,6 +17,43 @@ void main() {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class that can hold invocation information that an [UndoableAction] can
|
||||||
|
/// use to undo/redo itself.
|
||||||
|
///
|
||||||
|
/// Instances of this class are returned from [UndoableAction]s and placed on
|
||||||
|
/// the undo stack when they are invoked.
|
||||||
|
class Memento extends Object with DiagnosticableMixin implements Diagnosticable {
|
||||||
|
const Memento({
|
||||||
|
@required this.name,
|
||||||
|
@required this.undo,
|
||||||
|
@required this.redo,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns true if this Memento can be used to undo.
|
||||||
|
///
|
||||||
|
/// Subclasses could override to provide their own conditions when a command is
|
||||||
|
/// undoable.
|
||||||
|
bool get canUndo => true;
|
||||||
|
|
||||||
|
/// Returns true if this Memento can be used to redo.
|
||||||
|
///
|
||||||
|
/// Subclasses could override to provide their own conditions when a command is
|
||||||
|
/// redoable.
|
||||||
|
bool get canRedo => true;
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final VoidCallback undo;
|
||||||
|
final ValueGetter<Memento> redo;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties.add(StringProperty('name', name));
|
||||||
|
properties.add(FlagProperty('undo', value: undo != null, ifTrue: 'undo'));
|
||||||
|
properties.add(FlagProperty('redo', value: redo != null, ifTrue: 'redo'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Undoable Actions
|
/// Undoable Actions
|
||||||
|
|
||||||
/// An [ActionDispatcher] subclass that manages the invocation of undoable
|
/// An [ActionDispatcher] subclass that manages the invocation of undoable
|
||||||
@ -29,10 +69,10 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
|
|
||||||
// A stack of actions that have been performed. The most recent action
|
// A stack of actions that have been performed. The most recent action
|
||||||
// performed is at the end of the list.
|
// performed is at the end of the list.
|
||||||
final List<UndoableAction> _completedActions = <UndoableAction>[];
|
final DoubleLinkedQueue<Memento> _completedActions = DoubleLinkedQueue<Memento>();
|
||||||
// A stack of actions that can be redone. The most recent action performed is
|
// A stack of actions that can be redone. The most recent action performed is
|
||||||
// at the end of the list.
|
// at the end of the list.
|
||||||
final List<UndoableAction> _undoneActions = <UndoableAction>[];
|
final List<Memento> _undoneActions = <Memento>[];
|
||||||
|
|
||||||
static const int _defaultMaxUndoLevels = 1000;
|
static const int _defaultMaxUndoLevels = 1000;
|
||||||
|
|
||||||
@ -71,11 +111,11 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
|
Object invokeAction(Action<Intent> action, Intent intent, [BuildContext context]) {
|
||||||
final bool result = super.invokeAction(action, intent, focusNode: focusNode);
|
final Object result = super.invokeAction(action, intent, context);
|
||||||
print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this ');
|
print('Invoking ${action is UndoableAction ? 'undoable ' : ''}$intent as $action: $this ');
|
||||||
if (action is UndoableAction) {
|
if (action is UndoableAction) {
|
||||||
_completedActions.add(action);
|
_completedActions.addLast(result as Memento);
|
||||||
_undoneActions.clear();
|
_undoneActions.clear();
|
||||||
_pruneActions();
|
_pruneActions();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -86,15 +126,14 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
// Enforces undo level limit.
|
// Enforces undo level limit.
|
||||||
void _pruneActions() {
|
void _pruneActions() {
|
||||||
while (_completedActions.length > _maxUndoLevels) {
|
while (_completedActions.length > _maxUndoLevels) {
|
||||||
_completedActions.removeAt(0);
|
_completedActions.removeFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if there is an action on the stack that can be undone.
|
/// Returns true if there is an action on the stack that can be undone.
|
||||||
bool get canUndo {
|
bool get canUndo {
|
||||||
if (_completedActions.isNotEmpty) {
|
if (_completedActions.isNotEmpty) {
|
||||||
final Intent lastIntent = _completedActions.last.invocationIntent;
|
return _completedActions.first.canUndo;
|
||||||
return lastIntent.isEnabled(primaryFocus.context);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -102,8 +141,7 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
/// Returns true if an action that has been undone can be re-invoked.
|
/// Returns true if an action that has been undone can be re-invoked.
|
||||||
bool get canRedo {
|
bool get canRedo {
|
||||||
if (_undoneActions.isNotEmpty) {
|
if (_undoneActions.isNotEmpty) {
|
||||||
final Intent lastIntent = _undoneActions.last.invocationIntent;
|
return _undoneActions.first.canRedo;
|
||||||
return lastIntent.isEnabled(primaryFocus?.context);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -116,9 +154,9 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
if (!canUndo) {
|
if (!canUndo) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final UndoableAction action = _completedActions.removeLast();
|
final Memento memento = _completedActions.removeLast();
|
||||||
action.undo();
|
memento.undo();
|
||||||
_undoneActions.add(action);
|
_undoneActions.add(memento);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -131,9 +169,9 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
if (!canRedo) {
|
if (!canRedo) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final UndoableAction action = _undoneActions.removeLast();
|
final Memento memento = _undoneActions.removeLast();
|
||||||
action.invoke(action.invocationNode, action.invocationIntent);
|
final Memento replacement = memento.redo();
|
||||||
_completedActions.add(action);
|
_completedActions.add(replacement);
|
||||||
_pruneActions();
|
_pruneActions();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return true;
|
return true;
|
||||||
@ -144,71 +182,50 @@ class UndoableActionDispatcher extends ActionDispatcher implements Listenable {
|
|||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(IntProperty('undoable items', _completedActions.length));
|
properties.add(IntProperty('undoable items', _completedActions.length));
|
||||||
properties.add(IntProperty('redoable items', _undoneActions.length));
|
properties.add(IntProperty('redoable items', _undoneActions.length));
|
||||||
properties.add(IterableProperty<UndoableAction>('undo stack', _completedActions));
|
properties.add(IterableProperty<Memento>('undo stack', _completedActions));
|
||||||
properties.add(IterableProperty<UndoableAction>('redo stack', _undoneActions));
|
properties.add(IterableProperty<Memento>('redo stack', _undoneActions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UndoIntent extends Intent {
|
class UndoIntent extends Intent {
|
||||||
const UndoIntent() : super(kUndoActionKey);
|
const UndoIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class UndoAction extends Action<UndoIntent> {
|
||||||
|
@override
|
||||||
|
bool get enabled {
|
||||||
|
final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher;
|
||||||
|
return manager.canUndo;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isEnabled(BuildContext context) {
|
void invoke(UndoIntent intent) {
|
||||||
final UndoableActionDispatcher manager = Actions.of(context, nullOk: true) as UndoableActionDispatcher;
|
final UndoableActionDispatcher manager = Actions.of(primaryFocus?.context ?? FocusDemo.appKey.currentContext, nullOk: true) as UndoableActionDispatcher;
|
||||||
return manager.canUndo;
|
manager?.undo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RedoIntent extends Intent {
|
class RedoIntent extends Intent {
|
||||||
const RedoIntent() : super(kRedoActionKey);
|
const RedoIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class RedoAction extends Action<RedoIntent> {
|
||||||
|
@override
|
||||||
|
bool get enabled {
|
||||||
|
final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher;
|
||||||
|
return manager.canRedo;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isEnabled(BuildContext context) {
|
RedoAction invoke(RedoIntent intent) {
|
||||||
final UndoableActionDispatcher manager = Actions.of(context, nullOk: true) as UndoableActionDispatcher;
|
final UndoableActionDispatcher manager = Actions.of(primaryFocus.context, nullOk: true) as UndoableActionDispatcher;
|
||||||
return manager.canRedo;
|
manager?.redo();
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocalKey kUndoActionKey = ValueKey<String>('Undo');
|
|
||||||
const Intent kUndoIntent = UndoIntent();
|
|
||||||
final Action kUndoAction = CallbackAction(
|
|
||||||
kUndoActionKey,
|
|
||||||
onInvoke: (FocusNode node, Intent tag) {
|
|
||||||
if (node?.context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final UndoableActionDispatcher manager = Actions.of(node.context, nullOk: true) as UndoableActionDispatcher;
|
|
||||||
manager?.undo();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const LocalKey kRedoActionKey = ValueKey<String>('Redo');
|
|
||||||
const Intent kRedoIntent = RedoIntent();
|
|
||||||
final Action kRedoAction = CallbackAction(
|
|
||||||
kRedoActionKey,
|
|
||||||
onInvoke: (FocusNode node, Intent tag) {
|
|
||||||
if (node?.context == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final UndoableActionDispatcher manager = Actions.of(node.context, nullOk: true) as UndoableActionDispatcher;
|
|
||||||
manager?.redo();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/// An action that can be undone.
|
/// An action that can be undone.
|
||||||
abstract class UndoableAction extends Action {
|
abstract class UndoableAction<T extends Intent> extends Action<T> {
|
||||||
/// A const constructor to [UndoableAction].
|
|
||||||
///
|
|
||||||
/// The [intentKey] parameter must not be null.
|
|
||||||
UndoableAction(LocalKey intentKey) : super(intentKey);
|
|
||||||
|
|
||||||
/// The node supplied when this command was invoked.
|
|
||||||
FocusNode get invocationNode => _invocationNode;
|
|
||||||
FocusNode _invocationNode;
|
|
||||||
|
|
||||||
@protected
|
|
||||||
set invocationNode(FocusNode value) => _invocationNode = value;
|
|
||||||
|
|
||||||
/// The [Intent] this action was originally invoked with.
|
/// The [Intent] this action was originally invoked with.
|
||||||
Intent get invocationIntent => _invocationTag;
|
Intent get invocationIntent => _invocationTag;
|
||||||
Intent _invocationTag;
|
Intent _invocationTag;
|
||||||
@ -216,110 +233,63 @@ abstract class UndoableAction extends Action {
|
|||||||
@protected
|
@protected
|
||||||
set invocationIntent(Intent value) => _invocationTag = value;
|
set invocationIntent(Intent value) => _invocationTag = value;
|
||||||
|
|
||||||
/// Returns true if the data model can be returned to the state it was in
|
|
||||||
/// previous to this action being executed.
|
|
||||||
///
|
|
||||||
/// Default implementation returns true.
|
|
||||||
bool get undoable => true;
|
|
||||||
|
|
||||||
/// Reverts the data model to the state before this command executed.
|
|
||||||
@mustCallSuper
|
|
||||||
void undo();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@mustCallSuper
|
@mustCallSuper
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(T intent) {
|
||||||
invocationNode = node;
|
|
||||||
invocationIntent = intent;
|
invocationIntent = intent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UndoableFocusActionBase<T extends Intent> extends UndoableAction<T> {
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
@mustCallSuper
|
||||||
super.debugFillProperties(properties);
|
Memento invoke(T intent) {
|
||||||
properties.add(DiagnosticsProperty<FocusNode>('invocationNode', invocationNode));
|
super.invoke(intent);
|
||||||
|
final FocusNode previousFocus = primaryFocus;
|
||||||
|
return Memento(name: previousFocus.debugLabel, undo: () {
|
||||||
|
previousFocus.requestFocus();
|
||||||
|
}, redo: () {
|
||||||
|
return invoke(intent);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UndoableFocusActionBase extends UndoableAction {
|
class UndoableRequestFocusAction extends UndoableFocusActionBase<RequestFocusIntent> {
|
||||||
UndoableFocusActionBase(LocalKey name) : super(name);
|
|
||||||
|
|
||||||
FocusNode _previousFocus;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
Memento invoke(RequestFocusIntent intent) {
|
||||||
super.invoke(node, intent);
|
final Memento memento = super.invoke(intent);
|
||||||
_previousFocus = primaryFocus;
|
intent.focusNode.requestFocus();
|
||||||
node.requestFocus();
|
return memento;
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void undo() {
|
|
||||||
if (_previousFocus == null) {
|
|
||||||
primaryFocus?.unfocus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_previousFocus is FocusScopeNode) {
|
|
||||||
// The only way a scope can be the _previousFocus is if there was no
|
|
||||||
// focusedChild for the scope when we invoked this action, so we need to
|
|
||||||
// return to that state.
|
|
||||||
|
|
||||||
// Unfocus the current node to remove it from the focused child list of
|
|
||||||
// the scope.
|
|
||||||
primaryFocus?.unfocus();
|
|
||||||
// and then let the scope node be focused...
|
|
||||||
}
|
|
||||||
_previousFocus.requestFocus();
|
|
||||||
_previousFocus = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DiagnosticsProperty<FocusNode>('previous', _previousFocus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UndoableRequestFocusAction extends UndoableFocusActionBase {
|
|
||||||
UndoableRequestFocusAction() : super(RequestFocusAction.key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void invoke(FocusNode node, Intent intent) {
|
|
||||||
super.invoke(node, intent);
|
|
||||||
node.requestFocus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actions for manipulating focus.
|
/// Actions for manipulating focus.
|
||||||
class UndoableNextFocusAction extends UndoableFocusActionBase {
|
class UndoableNextFocusAction extends UndoableFocusActionBase<NextFocusIntent> {
|
||||||
UndoableNextFocusAction() : super(NextFocusAction.key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
Memento invoke(NextFocusIntent intent) {
|
||||||
super.invoke(node, intent);
|
final Memento memento = super.invoke(intent);
|
||||||
node.nextFocus();
|
primaryFocus.nextFocus();
|
||||||
|
return memento;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UndoablePreviousFocusAction extends UndoableFocusActionBase {
|
class UndoablePreviousFocusAction extends UndoableFocusActionBase<PreviousFocusIntent> {
|
||||||
UndoablePreviousFocusAction() : super(PreviousFocusAction.key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
Memento invoke(PreviousFocusIntent intent) {
|
||||||
super.invoke(node, intent);
|
final Memento memento = super.invoke(intent);
|
||||||
node.previousFocus();
|
primaryFocus.previousFocus();
|
||||||
|
return memento;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UndoableDirectionalFocusAction extends UndoableFocusActionBase {
|
class UndoableDirectionalFocusAction extends UndoableFocusActionBase<DirectionalFocusIntent> {
|
||||||
UndoableDirectionalFocusAction() : super(DirectionalFocusAction.key);
|
|
||||||
|
|
||||||
TraversalDirection direction;
|
TraversalDirection direction;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, DirectionalFocusIntent intent) {
|
Memento invoke(DirectionalFocusIntent intent) {
|
||||||
super.invoke(node, intent);
|
final Memento memento = super.invoke(intent);
|
||||||
final DirectionalFocusIntent args = intent;
|
primaryFocus.focusInDirection(intent.direction);
|
||||||
node.focusInDirection(args.direction);
|
return memento;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,6 +305,7 @@ class DemoButton extends StatefulWidget {
|
|||||||
|
|
||||||
class _DemoButtonState extends State<DemoButton> {
|
class _DemoButtonState extends State<DemoButton> {
|
||||||
FocusNode _focusNode;
|
FocusNode _focusNode;
|
||||||
|
final GlobalKey _nameKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -345,7 +316,7 @@ class _DemoButtonState extends State<DemoButton> {
|
|||||||
void _handleOnPressed() {
|
void _handleOnPressed() {
|
||||||
print('Button ${widget.name} pressed.');
|
print('Button ${widget.name} pressed.');
|
||||||
setState(() {
|
setState(() {
|
||||||
Actions.invoke(context, const Intent(RequestFocusAction.key), focusNode: _focusNode);
|
Actions.invoke(_nameKey.currentContext, RequestFocusIntent(_focusNode));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -362,7 +333,7 @@ class _DemoButtonState extends State<DemoButton> {
|
|||||||
focusColor: Colors.red,
|
focusColor: Colors.red,
|
||||||
hoverColor: Colors.blue,
|
hoverColor: Colors.blue,
|
||||||
onPressed: () => _handleOnPressed(),
|
onPressed: () => _handleOnPressed(),
|
||||||
child: Text(widget.name),
|
child: Text(widget.name, key: _nameKey),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,6 +341,8 @@ class _DemoButtonState extends State<DemoButton> {
|
|||||||
class FocusDemo extends StatefulWidget {
|
class FocusDemo extends StatefulWidget {
|
||||||
const FocusDemo({Key key}) : super(key: key);
|
const FocusDemo({Key key}) : super(key: key);
|
||||||
|
|
||||||
|
static GlobalKey appKey = GlobalKey();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_FocusDemoState createState() => _FocusDemoState();
|
_FocusDemoState createState() => _FocusDemoState();
|
||||||
}
|
}
|
||||||
@ -415,22 +388,23 @@ class _FocusDemoState extends State<FocusDemo> {
|
|||||||
final TextTheme textTheme = Theme.of(context).textTheme;
|
final TextTheme textTheme = Theme.of(context).textTheme;
|
||||||
return Actions(
|
return Actions(
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
RequestFocusAction.key: () => UndoableRequestFocusAction(),
|
RequestFocusIntent: UndoableRequestFocusAction(),
|
||||||
NextFocusAction.key: () => UndoableNextFocusAction(),
|
NextFocusIntent: UndoableNextFocusAction(),
|
||||||
PreviousFocusAction.key: () => UndoablePreviousFocusAction(),
|
PreviousFocusIntent: UndoablePreviousFocusAction(),
|
||||||
DirectionalFocusAction.key: () => UndoableDirectionalFocusAction(),
|
DirectionalFocusIntent: UndoableDirectionalFocusAction(),
|
||||||
kUndoActionKey: () => kUndoAction,
|
UndoIntent: UndoAction(),
|
||||||
kRedoActionKey: () => kRedoAction,
|
RedoIntent: RedoAction(),
|
||||||
},
|
},
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
child: Shortcuts(
|
child: Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): kRedoIntent,
|
LogicalKeySet(Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.shift, LogicalKeyboardKey.keyZ): const RedoIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): kUndoIntent,
|
LogicalKeySet(Platform.isMacOS ? LogicalKeyboardKey.meta : LogicalKeyboardKey.control, LogicalKeyboardKey.keyZ): const UndoIntent(),
|
||||||
},
|
},
|
||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
|
key: FocusDemo.appKey,
|
||||||
debugLabel: 'Scope',
|
debugLabel: 'Scope',
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
child: DefaultTextStyle(
|
child: DefaultTextStyle(
|
||||||
@ -477,7 +451,7 @@ class _FocusDemoState extends State<FocusDemo> {
|
|||||||
child: const Text('UNDO'),
|
child: const Text('UNDO'),
|
||||||
onPressed: canUndo
|
onPressed: canUndo
|
||||||
? () {
|
? () {
|
||||||
Actions.invoke(context, kUndoIntent);
|
Actions.invoke(context, const UndoIntent());
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@ -488,7 +462,7 @@ class _FocusDemoState extends State<FocusDemo> {
|
|||||||
child: const Text('REDO'),
|
child: const Text('REDO'),
|
||||||
onPressed: canRedo
|
onPressed: canRedo
|
||||||
? () {
|
? () {
|
||||||
Actions.invoke(context, kRedoIntent);
|
Actions.invoke(context, const RedoIntent());
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
@ -213,7 +213,7 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// shortcuts: <LogicalKeySet, Intent>{
|
/// shortcuts: <LogicalKeySet, Intent>{
|
||||||
/// ... WidgetsApp.defaultShortcuts,
|
/// ... WidgetsApp.defaultShortcuts,
|
||||||
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
|
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
/// },
|
/// },
|
||||||
/// color: const Color(0xFFFF0000),
|
/// color: const Color(0xFFFF0000),
|
||||||
/// builder: (BuildContext context, Widget child) {
|
/// builder: (BuildContext context, Widget child) {
|
||||||
@ -239,12 +239,12 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// Widget build(BuildContext context) {
|
/// Widget build(BuildContext context) {
|
||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// actions: <LocalKey, ActionFactory>{
|
/// actions: <Type, Action<Intent>>{
|
||||||
/// ... WidgetsApp.defaultActions,
|
/// ... WidgetsApp.defaultActions,
|
||||||
/// ActivateAction.key: () => CallbackAction(
|
/// ActivateAction: CallbackAction(
|
||||||
/// ActivateAction.key,
|
/// onInvoke: (Intent intent) {
|
||||||
/// onInvoke: (FocusNode focusNode, Intent intent) {
|
|
||||||
/// // Do something here...
|
/// // Do something here...
|
||||||
|
/// return null;
|
||||||
/// },
|
/// },
|
||||||
/// ),
|
/// ),
|
||||||
/// },
|
/// },
|
||||||
@ -257,7 +257,7 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
|
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
|
||||||
final Map<LocalKey, ActionFactory> actions;
|
final Map<Type, Action<Intent>> actions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_CupertinoAppState createState() => _CupertinoAppState();
|
_CupertinoAppState createState() => _CupertinoAppState();
|
||||||
|
@ -183,9 +183,9 @@ class ChangeNotifier implements Listenable {
|
|||||||
/// Call all the registered listeners.
|
/// Call all the registered listeners.
|
||||||
///
|
///
|
||||||
/// Call this method whenever the object changes, to notify any clients the
|
/// Call this method whenever the object changes, to notify any clients the
|
||||||
/// object may have. Listeners that are added during this iteration will not
|
/// object may have changed. Listeners that are added during this iteration
|
||||||
/// be visited. Listeners that are removed during this iteration will not be
|
/// will not be visited. Listeners that are removed during this iteration will
|
||||||
/// visited after they are removed.
|
/// not be visited after they are removed.
|
||||||
///
|
///
|
||||||
/// Exceptions thrown by listeners will be caught and reported using
|
/// Exceptions thrown by listeners will be caught and reported using
|
||||||
/// [FlutterError.reportError].
|
/// [FlutterError.reportError].
|
||||||
|
@ -476,7 +476,7 @@ class MaterialApp extends StatefulWidget {
|
|||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// shortcuts: <LogicalKeySet, Intent>{
|
/// shortcuts: <LogicalKeySet, Intent>{
|
||||||
/// ... WidgetsApp.defaultShortcuts,
|
/// ... WidgetsApp.defaultShortcuts,
|
||||||
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
|
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
/// },
|
/// },
|
||||||
/// color: const Color(0xFFFF0000),
|
/// color: const Color(0xFFFF0000),
|
||||||
/// builder: (BuildContext context, Widget child) {
|
/// builder: (BuildContext context, Widget child) {
|
||||||
@ -502,12 +502,12 @@ class MaterialApp extends StatefulWidget {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// Widget build(BuildContext context) {
|
/// Widget build(BuildContext context) {
|
||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// actions: <LocalKey, ActionFactory>{
|
/// actions: <Type, Action<Intent>>{
|
||||||
/// ... WidgetsApp.defaultActions,
|
/// ... WidgetsApp.defaultActions,
|
||||||
/// ActivateAction.key: () => CallbackAction(
|
/// ActivateAction: CallbackAction(
|
||||||
/// ActivateAction.key,
|
/// onInvoke: (Intent intent) {
|
||||||
/// onInvoke: (FocusNode focusNode, Intent intent) {
|
|
||||||
/// // Do something here...
|
/// // Do something here...
|
||||||
|
/// return null;
|
||||||
/// },
|
/// },
|
||||||
/// ),
|
/// ),
|
||||||
/// },
|
/// },
|
||||||
@ -520,7 +520,7 @@ class MaterialApp extends StatefulWidget {
|
|||||||
/// ```
|
/// ```
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
|
/// {@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
|
/// Turns on a [GridPaper] overlay that paints a baseline grid
|
||||||
/// Material apps.
|
/// Material apps.
|
||||||
|
@ -169,17 +169,17 @@ class Checkbox extends StatefulWidget {
|
|||||||
|
|
||||||
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
||||||
bool get enabled => widget.onChanged != null;
|
bool get enabled => widget.onChanged != null;
|
||||||
Map<LocalKey, ActionFactory> _actionMap;
|
Map<Type, Action<Intent>> _actionMap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_actionMap = <LocalKey, ActionFactory>{
|
_actionMap = <Type, Action<Intent>>{
|
||||||
ActivateAction.key: _createAction,
|
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void _actionHandler(FocusNode node, Intent intent){
|
void _actionHandler(ActivateIntent intent) {
|
||||||
if (widget.onChanged != null) {
|
if (widget.onChanged != null) {
|
||||||
switch (widget.value) {
|
switch (widget.value) {
|
||||||
case false:
|
case false:
|
||||||
@ -193,17 +193,10 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final RenderObject renderObject = node.context.findRenderObject();
|
final RenderObject renderObject = context.findRenderObject();
|
||||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
Action _createAction() {
|
|
||||||
return CallbackAction(
|
|
||||||
ActivateAction.key,
|
|
||||||
onInvoke: _actionHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _focused = false;
|
bool _focused = false;
|
||||||
void _handleFocusHighlightChanged(bool focused) {
|
void _handleFocusHighlightChanged(bool focused) {
|
||||||
if (focused != _focused) {
|
if (focused != _focused) {
|
||||||
|
@ -158,7 +158,7 @@ class _DropdownMenuItemButtonState<T> extends State<_DropdownMenuItemButton<T>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
static final Map<LogicalKeySet, Intent> _webShortcuts =<LogicalKeySet, Intent>{
|
static final Map<LogicalKeySet, Intent> _webShortcuts =<LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1080,7 +1080,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
FocusNode _internalNode;
|
FocusNode _internalNode;
|
||||||
FocusNode get focusNode => widget.focusNode ?? _internalNode;
|
FocusNode get focusNode => widget.focusNode ?? _internalNode;
|
||||||
bool _hasPrimaryFocus = false;
|
bool _hasPrimaryFocus = false;
|
||||||
Map<LocalKey, ActionFactory> _actionMap;
|
Map<Type, Action<Intent>> _actionMap;
|
||||||
FocusHighlightMode _focusHighlightMode;
|
FocusHighlightMode _focusHighlightMode;
|
||||||
|
|
||||||
// Only used if needed to create _internalNode.
|
// Only used if needed to create _internalNode.
|
||||||
@ -1095,8 +1095,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||||||
if (widget.focusNode == null) {
|
if (widget.focusNode == null) {
|
||||||
_internalNode ??= _createFocusNode();
|
_internalNode ??= _createFocusNode();
|
||||||
}
|
}
|
||||||
_actionMap = <LocalKey, ActionFactory>{
|
_actionMap = <Type, Action<Intent>>{
|
||||||
ActivateAction.key: _createAction,
|
ActivateIntent: CallbackAction<ActivateIntent>(
|
||||||
|
onInvoke: (ActivateIntent intent) => _handleTap(),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
focusNode.addListener(_handleFocusChanged);
|
focusNode.addListener(_handleFocusChanged);
|
||||||
final FocusManager focusManager = WidgetsBinding.instance.focusManager;
|
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
|
// 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.
|
// _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
|
// 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;
|
InteractiveInkFeature _currentSplash;
|
||||||
bool _hovering = false;
|
bool _hovering = false;
|
||||||
final Map<_HighlightType, InkHighlight> _highlights = <_HighlightType, InkHighlight>{};
|
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;
|
bool get highlightsExist => _highlights.values.where((InkHighlight highlight) => highlight != null).isNotEmpty;
|
||||||
|
|
||||||
void _handleAction(FocusNode node, Intent intent) {
|
void _handleAction(ActivateIntent intent) {
|
||||||
_startSplash(context: node.context);
|
_startSplash(context: context);
|
||||||
_handleTap(node.context);
|
_handleTap(context);
|
||||||
}
|
|
||||||
|
|
||||||
Action _createAction() {
|
|
||||||
return CallbackAction(
|
|
||||||
ActivateAction.key,
|
|
||||||
onInvoke: _handleAction,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_actionMap = <LocalKey, ActionFactory>{
|
_actionMap = <Type, Action<Intent>>{
|
||||||
ActivateAction.key: _createAction,
|
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _handleAction),
|
||||||
};
|
};
|
||||||
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
|
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange);
|
||||||
}
|
}
|
||||||
|
@ -262,31 +262,26 @@ class Radio<T> extends StatefulWidget {
|
|||||||
|
|
||||||
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin {
|
||||||
bool get enabled => widget.onChanged != null;
|
bool get enabled => widget.onChanged != null;
|
||||||
Map<LocalKey, ActionFactory> _actionMap;
|
Map<Type, Action<Intent>> _actionMap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_actionMap = <LocalKey, ActionFactory>{
|
_actionMap = <Type, Action<Intent>>{
|
||||||
ActivateAction.key: _createAction,
|
ActivateIntent: CallbackAction<ActivateIntent>(
|
||||||
|
onInvoke: _actionHandler,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void _actionHandler(FocusNode node, Intent intent) {
|
void _actionHandler(ActivateIntent intent) {
|
||||||
if (widget.onChanged != null) {
|
if (widget.onChanged != null) {
|
||||||
widget.onChanged(widget.value);
|
widget.onChanged(widget.value);
|
||||||
}
|
}
|
||||||
final RenderObject renderObject = node.context.findRenderObject();
|
final RenderObject renderObject = context.findRenderObject();
|
||||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
Action _createAction() {
|
|
||||||
return CallbackAction(
|
|
||||||
ActivateAction.key,
|
|
||||||
onInvoke: _actionHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _focused = false;
|
bool _focused = false;
|
||||||
void _handleHighlightChanged(bool focused) {
|
void _handleHighlightChanged(bool focused) {
|
||||||
if (_focused != focused) {
|
if (_focused != focused) {
|
||||||
|
@ -230,31 +230,24 @@ class Switch extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
class _SwitchState extends State<Switch> with TickerProviderStateMixin {
|
||||||
Map<LocalKey, ActionFactory> _actionMap;
|
Map<Type, Action<Intent>> _actionMap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_actionMap = <LocalKey, ActionFactory>{
|
_actionMap = <Type, Action<Intent>>{
|
||||||
ActivateAction.key: _createAction,
|
ActivateIntent: CallbackAction<ActivateIntent>(onInvoke: _actionHandler),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void _actionHandler(FocusNode node, Intent intent){
|
void _actionHandler(ActivateIntent intent) {
|
||||||
if (widget.onChanged != null) {
|
if (widget.onChanged != null) {
|
||||||
widget.onChanged(!widget.value);
|
widget.onChanged(!widget.value);
|
||||||
}
|
}
|
||||||
final RenderObject renderObject = node.context.findRenderObject();
|
final RenderObject renderObject = context.findRenderObject();
|
||||||
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
renderObject.sendSemanticsEvent(const TapSemanticEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
Action _createAction() {
|
|
||||||
return CallbackAction(
|
|
||||||
ActivateAction.key,
|
|
||||||
onInvoke: _actionHandler,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _focused = false;
|
bool _focused = false;
|
||||||
void _handleFocusHighlightChanged(bool focused) {
|
void _handleFocusHighlightChanged(bool focused) {
|
||||||
if (focused != _focused) {
|
if (focused != _focused) {
|
||||||
|
@ -78,7 +78,7 @@ export 'package:flutter/services.dart' show SmartQuotesType, SmartDashesType;
|
|||||||
/// shortcuts: <LogicalKeySet, Intent>{
|
/// shortcuts: <LogicalKeySet, Intent>{
|
||||||
/// // Pressing enter on the field will now move to the next field.
|
/// // Pressing enter on the field will now move to the next field.
|
||||||
/// LogicalKeySet(LogicalKeyboardKey.enter):
|
/// LogicalKeySet(LogicalKeyboardKey.enter):
|
||||||
/// Intent(NextFocusAction.key),
|
/// NextFocusIntent(),
|
||||||
/// },
|
/// },
|
||||||
/// child: FocusTraversalGroup(
|
/// child: FocusTraversalGroup(
|
||||||
/// child: Form(
|
/// child: Form(
|
||||||
|
@ -12,48 +12,44 @@ import 'focus_scope.dart';
|
|||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
|
|
||||||
/// Creates actions for use in defining shortcuts.
|
// BuildContext/Element doesn't have a parent accessor, but it can be
|
||||||
///
|
// simulated with visitAncestorElements. _getParent is needed because
|
||||||
/// Used by clients of [ShortcutMap] to define shortcut maps.
|
// context.getElementForInheritedWidgetOfExactType will return itself if it
|
||||||
typedef ActionFactory = Action Function();
|
// happens to be of the correct type. getParent should be O(1), since we
|
||||||
|
// always return false at the first ancestor.
|
||||||
|
BuildContext _getParent(BuildContext context) {
|
||||||
|
BuildContext parent;
|
||||||
|
context.visitAncestorElements((Element ancestor) {
|
||||||
|
parent = ancestor;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
/// A class representing a particular configuration of an action.
|
/// A class representing a particular configuration of an action.
|
||||||
///
|
///
|
||||||
/// This class is what a key map in a [ShortcutMap] has as values, and is used
|
/// This class is what a key map in a [ShortcutMap] has as values, and is used
|
||||||
/// by an [ActionDispatcher] to look up an action and invoke it, giving it this
|
/// by an [ActionDispatcher] to look up an action and invoke it, giving it this
|
||||||
/// object to extract configuration information from.
|
/// object to extract configuration information from.
|
||||||
///
|
@immutable
|
||||||
/// If this intent returns false from [isEnabled], then its associated action will
|
|
||||||
/// not be invoked if requested.
|
|
||||||
class Intent with Diagnosticable {
|
class Intent with Diagnosticable {
|
||||||
/// A const constructor for an [Intent].
|
/// A const constructor for an [Intent].
|
||||||
///
|
const Intent();
|
||||||
/// The [key] argument must not be null.
|
|
||||||
const Intent(this.key) : assert(key != null);
|
|
||||||
|
|
||||||
/// An intent that can't be mapped to an action.
|
/// An intent that can't be mapped to an action.
|
||||||
///
|
///
|
||||||
/// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
|
/// This Intent is mapped to an action in the [WidgetsApp] that does nothing,
|
||||||
/// so that it can be bound to a key in a [Shortcuts] widget in order to
|
/// so that it can be bound to a key in a [Shortcuts] widget in order to
|
||||||
/// disable a key binding made above it in the hierarchy.
|
/// disable a key binding made above it in the hierarchy.
|
||||||
static const Intent doNothing = Intent(DoNothingAction.key);
|
static const DoNothingIntent doNothing = DoNothingIntent._();
|
||||||
|
|
||||||
/// The key for the action this intent is associated with.
|
|
||||||
final LocalKey key;
|
|
||||||
|
|
||||||
/// Returns true if the associated action is able to be executed in the
|
|
||||||
/// given `context`.
|
|
||||||
///
|
|
||||||
/// Returns true by default.
|
|
||||||
bool isEnabled(BuildContext context) => true;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
||||||
super.debugFillProperties(properties);
|
|
||||||
properties.add(DiagnosticsProperty<LocalKey>('key', key));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The kind of callback that an [Action] uses to notify of changes to the
|
||||||
|
/// action's state.
|
||||||
|
///
|
||||||
|
/// To register an action listener, call [Action.addActionListener].
|
||||||
|
typedef ActionListenerCallback = void Function(Action<Intent> action);
|
||||||
|
|
||||||
/// Base class for actions.
|
/// Base class for actions.
|
||||||
///
|
///
|
||||||
/// As the name implies, an [Action] is an action or command to be performed.
|
/// As the name implies, an [Action] is an action or command to be performed.
|
||||||
@ -71,46 +67,266 @@ class Intent with Diagnosticable {
|
|||||||
/// up key combinations in order to invoke actions.
|
/// up key combinations in order to invoke actions.
|
||||||
/// * [Actions], which is a widget that defines a map of [Intent] to [Action]
|
/// * [Actions], which is a widget that defines a map of [Intent] to [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 using a
|
/// * [ActionDispatcher], a class that takes an [Action] and invokes it, passing
|
||||||
/// [FocusNode] for context.
|
/// a given [Intent].
|
||||||
abstract class Action with Diagnosticable {
|
abstract class Action<T extends Intent> with Diagnosticable {
|
||||||
/// A const constructor for an [Action].
|
final ObserverList<ActionListenerCallback> _listeners = ObserverList<ActionListenerCallback>();
|
||||||
///
|
|
||||||
/// The [intentKey] parameter must not be null.
|
|
||||||
const Action(this.intentKey) : assert(intentKey != null);
|
|
||||||
|
|
||||||
/// The unique key for this action.
|
/// Gets the type of intent this action responds to.
|
||||||
|
Type get intentType => T;
|
||||||
|
|
||||||
|
/// Returns true if the action is enabled and is ready to be invoked.
|
||||||
///
|
///
|
||||||
/// This key will be used to map to this action in an [ActionDispatcher].
|
/// This will be called by the [ActionDispatcher] before attempting to invoke
|
||||||
final LocalKey intentKey;
|
/// the action.
|
||||||
|
///
|
||||||
|
/// If the enabled state changes, overriding subclasses must call
|
||||||
|
/// [notifyActionListeners] to notify any listeners of the change.
|
||||||
|
bool get enabled => true;
|
||||||
|
|
||||||
/// Called when the action is to be performed.
|
/// Called when the action is to be performed.
|
||||||
///
|
///
|
||||||
/// This is called by the [ActionDispatcher] when an action is accepted by a
|
/// This is called by the [ActionDispatcher] when an action is invoked via
|
||||||
/// [FocusNode] by returning true from its `onAction` callback, or when an
|
/// [Actions.invoke], or when an action is invoked using
|
||||||
/// action is invoked using [ActionDispatcher.invokeAction].
|
/// [ActionDispatcher.invokeAction] directly.
|
||||||
///
|
///
|
||||||
/// This method is only meant to be invoked by an [ActionDispatcher], or by
|
/// This method is only meant to be invoked by an [ActionDispatcher], or by
|
||||||
/// subclasses.
|
/// its subclasses, and only when [enabled] is true.
|
||||||
///
|
///
|
||||||
/// Actions invoked directly with [ActionDispatcher.invokeAction] may receive a
|
/// When overriding this method, the returned value can be any Object, but
|
||||||
/// null `node`. If the information available from a focus node is
|
/// changing the return type of the override to match the type of the returned
|
||||||
/// needed in the action, use [ActionDispatcher.invokeFocusedAction] instead.
|
/// value provides more type safety.
|
||||||
|
///
|
||||||
|
/// For instance, if your override of `invoke` returns an `int`, then define
|
||||||
|
/// it like so:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// class IncrementIntent extends Intent {
|
||||||
|
/// const IncrementIntent({this.index});
|
||||||
|
///
|
||||||
|
/// final int index;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// class MyIncrementAction extends Action<IncrementIntent> {
|
||||||
|
/// @override
|
||||||
|
/// int invoke(IncrementIntent intent) {
|
||||||
|
/// return intent.index + 1;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
@protected
|
@protected
|
||||||
void invoke(FocusNode node, covariant Intent intent);
|
Object invoke(covariant T intent);
|
||||||
|
|
||||||
@override
|
/// Register a callback to listen for changes to the state of this action.
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
///
|
||||||
super.debugFillProperties(properties);
|
/// If you call this, you must call [removeActionListener] a matching number
|
||||||
properties.add(DiagnosticsProperty<LocalKey>('intentKey', intentKey));
|
/// of times, or memory leaks will occur. To help manage this and avoid memory
|
||||||
|
/// leaks, use of the [ActionListener] widget to register and unregister your
|
||||||
|
/// listener appropriately is highly recommended.
|
||||||
|
///
|
||||||
|
/// {@template flutter.widgets.actions.multipleAdds}
|
||||||
|
/// If a listener had been added twice, and is removed once during an
|
||||||
|
/// iteration (i.e. in response to a notification), it will still be called
|
||||||
|
/// again. If, on the other hand, it is removed as many times as it was
|
||||||
|
/// registered, then it will no longer be called. This odd behavior is the
|
||||||
|
/// result of the [Action] not being able to determine which listener
|
||||||
|
/// is being removed, since they are identical, and therefore conservatively
|
||||||
|
/// still calling all the listeners when it knows that any are still
|
||||||
|
/// registered.
|
||||||
|
///
|
||||||
|
/// This surprising behavior can be unexpectedly observed when registering a
|
||||||
|
/// listener on two separate objects which are both forwarding all
|
||||||
|
/// registrations to a common upstream object.
|
||||||
|
/// {@endtemplate}
|
||||||
|
@mustCallSuper
|
||||||
|
void addActionListener(ActionListenerCallback listener) => _listeners.add(listener);
|
||||||
|
|
||||||
|
/// Remove a previously registered closure from the list of closures that are
|
||||||
|
/// notified when the object changes.
|
||||||
|
///
|
||||||
|
/// If the given listener is not registered, the call is ignored.
|
||||||
|
///
|
||||||
|
/// If you call [addActionListener], you must call this method a matching
|
||||||
|
/// number of times, or memory leaks will occur. To help manage this and avoid
|
||||||
|
/// memory leaks, use of the [ActionListener] widget to register and
|
||||||
|
/// unregister your listener appropriately is highly recommended.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.actions.multipleAdds}
|
||||||
|
@mustCallSuper
|
||||||
|
void removeActionListener(ActionListenerCallback listener) => _listeners.remove(listener);
|
||||||
|
|
||||||
|
/// Call all the registered listeners.
|
||||||
|
///
|
||||||
|
/// Subclasses should call this method whenever the object changes, to notify
|
||||||
|
/// any clients the 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].
|
||||||
|
///
|
||||||
|
/// Surprising behavior can result when reentrantly removing a listener (i.e.
|
||||||
|
/// in response to a notification) that has been registered multiple times.
|
||||||
|
/// See the discussion at [removeActionListener].
|
||||||
|
@protected
|
||||||
|
@visibleForTesting
|
||||||
|
void notifyActionListeners() {
|
||||||
|
if (_listeners.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a local copy so that a listener can unregister while the list is
|
||||||
|
// being iterated over.
|
||||||
|
final List<ActionListenerCallback> localListeners = List<ActionListenerCallback>.from(_listeners);
|
||||||
|
for (final ActionListenerCallback listener in localListeners) {
|
||||||
|
InformationCollector collector;
|
||||||
|
assert(() {
|
||||||
|
collector = () sync* {
|
||||||
|
yield DiagnosticsProperty<Action<T>>(
|
||||||
|
'The $runtimeType sending notification was',
|
||||||
|
this,
|
||||||
|
style: DiagnosticsTreeStyle.errorProperty,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
try {
|
||||||
|
if (_listeners.contains(listener)) {
|
||||||
|
listener(this);
|
||||||
|
}
|
||||||
|
} catch (exception, stack) {
|
||||||
|
FlutterError.reportError(FlutterErrorDetails(
|
||||||
|
exception: exception,
|
||||||
|
stack: stack,
|
||||||
|
library: 'widgets library',
|
||||||
|
context: ErrorDescription('while dispatching notifications for $runtimeType'),
|
||||||
|
informationCollector: collector,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A helper widget for making sure that listeners on an action are removed properly.
|
||||||
|
///
|
||||||
|
/// Listeners on the [Action] class must have their listener callbacks removed
|
||||||
|
/// with [Action.removeActionListener] when the listener is disposed of. This widget
|
||||||
|
/// helps with that, by providing a lifetime for the connection between the
|
||||||
|
/// [listener] and the [Action], and by handling the adding and removing of
|
||||||
|
/// the [listener] at the right points in the widget lifecycle.
|
||||||
|
///
|
||||||
|
/// If you listen to an [Action] widget in a widget hierarchy, you should use
|
||||||
|
/// this widget. If you are using an [Action] outside of a widget context, then
|
||||||
|
/// you must call removeListener yourself.
|
||||||
|
@immutable
|
||||||
|
class ActionListener extends StatefulWidget {
|
||||||
|
/// Create a const [ActionListener].
|
||||||
|
///
|
||||||
|
/// The [listener], [action], and [child] arguments must not be null.
|
||||||
|
const ActionListener({
|
||||||
|
Key key,
|
||||||
|
@required this.listener,
|
||||||
|
@required this.action,
|
||||||
|
@required this.child,
|
||||||
|
}) : assert(listener != null),
|
||||||
|
assert(action != null),
|
||||||
|
assert(child != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The [ActionListenerCallback] callback to register with the [action].
|
||||||
|
///
|
||||||
|
/// Must not be null.
|
||||||
|
final ActionListenerCallback listener;
|
||||||
|
|
||||||
|
/// The [Action] that the callback will be registered with.
|
||||||
|
///
|
||||||
|
/// Must not be null.
|
||||||
|
final Action<Intent> action;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ActionListenerState createState() => _ActionListenerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionListenerState extends State<ActionListener> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
widget.action.addActionListener(widget.listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ActionListener oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.action == widget.action && oldWidget.listener == widget.listener) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
oldWidget.action.removeActionListener(oldWidget.listener);
|
||||||
|
widget.action.addActionListener(widget.listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.action.removeActionListener(widget.listener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.child;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An abstract [Action] subclass that adds an optional [BuildContext] to the
|
||||||
|
/// [invoke] method to be able to provide context to actions.
|
||||||
|
///
|
||||||
|
/// [ActionDispatcher.invokeAction] checks to see if the action it is invoking
|
||||||
|
/// is a [ContextAction], and if it is, supplies it with a context.
|
||||||
|
abstract class ContextAction<T extends Intent> extends Action<T> {
|
||||||
|
/// Called when the action is to be performed.
|
||||||
|
///
|
||||||
|
/// This is called by the [ActionDispatcher] when an action is invoked via
|
||||||
|
/// [Actions.invoke], or when an action is invoked using
|
||||||
|
/// [ActionDispatcher.invokeAction] directly.
|
||||||
|
///
|
||||||
|
/// This method is only meant to be invoked by an [ActionDispatcher], or by
|
||||||
|
/// its subclasses, and only when [enabled] is true.
|
||||||
|
///
|
||||||
|
/// The optional `context` parameter is the context of the invocation of the
|
||||||
|
/// action, and in the case of an action invoked by a [ShortcutsManager], via
|
||||||
|
/// a [Shortcuts] widget, will be the context of the [Shortcuts] widget.
|
||||||
|
///
|
||||||
|
/// When overriding this method, the returned value can be any Object, but
|
||||||
|
/// changing the return type of the override to match the type of the returned
|
||||||
|
/// value provides more type safety.
|
||||||
|
///
|
||||||
|
/// For instance, if your override of `invoke` returns an `int`, then define
|
||||||
|
/// it like so:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// class IncrementIntent extends Intent {
|
||||||
|
/// const IncrementIntent({this.index});
|
||||||
|
///
|
||||||
|
/// final int index;
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// class MyIncrementAction extends ContextAction<IncrementIntent> {
|
||||||
|
/// @override
|
||||||
|
/// int invoke(IncrementIntent intent, [BuildContext context]) {
|
||||||
|
/// return intent.index + 1;
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
@protected
|
||||||
|
@override
|
||||||
|
Object invoke(covariant T intent, [BuildContext context]);
|
||||||
|
}
|
||||||
|
|
||||||
/// The signature of a callback accepted by [CallbackAction].
|
/// The signature of a callback accepted by [CallbackAction].
|
||||||
typedef OnInvokeCallback = void Function(FocusNode node, Intent tag);
|
typedef OnInvokeCallback<T extends Intent> = Object Function(T intent);
|
||||||
|
|
||||||
/// An [Action] that takes a callback in order to configure it without having to
|
/// An [Action] that takes a callback in order to configure it without having to
|
||||||
/// subclass it.
|
/// create an explicit [Action] subclass just to call a callback.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
@ -120,48 +336,57 @@ typedef OnInvokeCallback = void Function(FocusNode node, Intent tag);
|
|||||||
/// 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 using a
|
/// * [ActionDispatcher], a class that takes an [Action] and invokes it using a
|
||||||
/// [FocusNode] for context.
|
/// [FocusNode] for context.
|
||||||
class CallbackAction extends Action {
|
class CallbackAction<T extends Intent> extends Action<T> {
|
||||||
/// A const constructor for an [Action].
|
/// A constructor for a [CallbackAction].
|
||||||
///
|
///
|
||||||
/// The `intentKey` and [onInvoke] parameters must not be null.
|
/// The `intentKey` and [onInvoke] parameters must not be null.
|
||||||
/// The [onInvoke] parameter is required.
|
/// The [onInvoke] parameter is required.
|
||||||
const CallbackAction(LocalKey intentKey, {@required this.onInvoke})
|
CallbackAction({@required this.onInvoke}) : assert(onInvoke != null);
|
||||||
: assert(onInvoke != null),
|
|
||||||
super(intentKey);
|
|
||||||
|
|
||||||
/// The callback to be called when invoked.
|
/// The callback to be called when invoked.
|
||||||
///
|
///
|
||||||
/// Must not be null.
|
/// Must not be null.
|
||||||
@protected
|
@protected
|
||||||
final OnInvokeCallback onInvoke;
|
final OnInvokeCallback<T> onInvoke;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) => onInvoke.call(node, intent);
|
Object invoke(covariant T intent) => onInvoke(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An action manager that simply invokes the actions given to it.
|
/// An action dispatcher that simply invokes the actions given to it.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// - [ShortcutManager], that uses this class to invoke actions.
|
||||||
|
/// - [Shortcuts] widget, which defines key mappings to [Intent]s.
|
||||||
|
/// - [Actions] widget, which defines a mapping between a in [Intent] type and
|
||||||
|
/// an [Action].
|
||||||
class ActionDispatcher with Diagnosticable {
|
class ActionDispatcher with Diagnosticable {
|
||||||
/// Const constructor so that subclasses can be const.
|
/// Const constructor so that subclasses can be immutable.
|
||||||
const ActionDispatcher();
|
const ActionDispatcher();
|
||||||
|
|
||||||
/// Invokes the given action, optionally without regard for the currently
|
/// Invokes the given `action`, passing it the given `intent`.
|
||||||
/// focused node in the focus tree.
|
|
||||||
///
|
///
|
||||||
/// Actions invoked will receive the given `focusNode`, or the
|
/// The action will be invoked with the given `context`, if given, but only if
|
||||||
/// [FocusManager.primaryFocus] if the given `focusNode` is null.
|
/// the action is a [ContextAction] subclass. If no `context` is given, and
|
||||||
|
/// the action is a [ContextAction], then the context from the [primaryFocus]
|
||||||
|
/// is used.
|
||||||
///
|
///
|
||||||
/// The `action` and `intent` arguments must not be null.
|
/// Returns the object returned from [Action.invoke] if the action was
|
||||||
///
|
/// successfully invoked, and null if the action is not enabled. May also
|
||||||
/// Returns true if the action was successfully invoked.
|
/// return null if [Action.invoke] returns null.
|
||||||
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
|
Object invokeAction(covariant Action<Intent> action, covariant Intent intent, [BuildContext context]) {
|
||||||
assert(action != null);
|
assert(action != null);
|
||||||
assert(intent != null);
|
assert(intent != null);
|
||||||
focusNode ??= primaryFocus;
|
context ??= primaryFocus?.context;
|
||||||
if (action != null && intent.isEnabled(focusNode.context)) {
|
if (action.enabled) {
|
||||||
action.invoke(focusNode, intent);
|
if (action is ContextAction) {
|
||||||
return true;
|
return action.invoke(intent, context);
|
||||||
|
} else {
|
||||||
|
return action.invoke(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +404,7 @@ class ActionDispatcher with Diagnosticable {
|
|||||||
/// * [Intent], a class that holds a unique [LocalKey] identifying an action,
|
/// * [Intent], a class that holds a unique [LocalKey] identifying an action,
|
||||||
/// as well as configuration information for running the [Action].
|
/// as well as configuration information for running the [Action].
|
||||||
/// * [Shortcuts], a widget used to bind key combinations to [Intent]s.
|
/// * [Shortcuts], a widget used to bind key combinations to [Intent]s.
|
||||||
class Actions extends InheritedWidget {
|
class Actions extends StatefulWidget {
|
||||||
/// Creates an [Actions] widget.
|
/// Creates an [Actions] widget.
|
||||||
///
|
///
|
||||||
/// The [child], [actions], and [dispatcher] arguments must not be null.
|
/// The [child], [actions], and [dispatcher] arguments must not be null.
|
||||||
@ -187,9 +412,10 @@ class Actions extends InheritedWidget {
|
|||||||
Key key,
|
Key key,
|
||||||
this.dispatcher,
|
this.dispatcher,
|
||||||
@required this.actions,
|
@required this.actions,
|
||||||
@required Widget child,
|
@required this.child,
|
||||||
}) : assert(actions != null),
|
}) : assert(actions != null),
|
||||||
super(key: key, child: child);
|
assert(child != null),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
/// The [ActionDispatcher] object that invokes actions.
|
/// The [ActionDispatcher] object that invokes actions.
|
||||||
///
|
///
|
||||||
@ -202,42 +428,110 @@ class Actions extends InheritedWidget {
|
|||||||
final ActionDispatcher dispatcher;
|
final ActionDispatcher dispatcher;
|
||||||
|
|
||||||
/// {@template flutter.widgets.actions.actions}
|
/// {@template flutter.widgets.actions.actions}
|
||||||
/// A map of [Intent] keys to [ActionFactory] factory methods that defines
|
/// A map of [Intent] keys to [Action<Intent>] objects that defines which
|
||||||
/// which actions this widget knows about.
|
/// actions this widget knows about.
|
||||||
///
|
///
|
||||||
/// For performance reasons, it is recommended that a pre-built map is
|
/// For performance reasons, it is recommended that a pre-built map is
|
||||||
/// passed in here (e.g. a final variable from your widget class) instead of
|
/// passed in here (e.g. a final variable from your widget class) instead of
|
||||||
/// defining it inline in the build function.
|
/// defining it inline in the build function.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final Map<LocalKey, ActionFactory> actions;
|
final Map<Type, Action<Intent>> actions;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
// Visits the Actions widget ancestors of the given element using
|
||||||
|
// getElementForInheritedWidgetOfExactType. Returns true if the visitor found
|
||||||
|
// what it was looking for.
|
||||||
|
static bool _visitActionsAncestors(BuildContext context, bool visitor(InheritedElement element)) {
|
||||||
|
InheritedElement actionsElement = context.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
|
||||||
|
while (actionsElement != null) {
|
||||||
|
if (visitor(actionsElement) == true) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// _getParent is needed here because
|
||||||
|
// context.getElementForInheritedWidgetOfExactType will return itself if it
|
||||||
|
// happens to be of the correct type.
|
||||||
|
final BuildContext parent = _getParent(actionsElement);
|
||||||
|
actionsElement = parent.getElementForInheritedWidgetOfExactType<_ActionsMarker>();
|
||||||
|
}
|
||||||
|
return actionsElement != null;
|
||||||
|
}
|
||||||
|
|
||||||
// Finds the nearest valid ActionDispatcher, or creates a new one if it
|
// Finds the nearest valid ActionDispatcher, or creates a new one if it
|
||||||
// doesn't find one.
|
// doesn't find one.
|
||||||
static ActionDispatcher _findDispatcher(Element element) {
|
static ActionDispatcher _findDispatcher(BuildContext context) {
|
||||||
assert(element.widget is Actions);
|
ActionDispatcher dispatcher;
|
||||||
final Actions actions = element.widget as Actions;
|
_visitActionsAncestors(context, (InheritedElement element) {
|
||||||
ActionDispatcher dispatcher = actions.dispatcher;
|
final ActionDispatcher found = (element.widget as _ActionsMarker).dispatcher;
|
||||||
if (dispatcher == null) {
|
if (found != null) {
|
||||||
bool visitAncestorElement(Element visitedElement) {
|
dispatcher = found;
|
||||||
if (visitedElement.widget is! Actions) {
|
return true;
|
||||||
// Continue visiting.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final Actions actions = visitedElement.widget as Actions;
|
|
||||||
if (actions.dispatcher == null) {
|
|
||||||
// Continue visiting.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
dispatcher = actions.dispatcher;
|
|
||||||
// Stop visiting.
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
element.visitAncestorElements(visitAncestorElement);
|
});
|
||||||
}
|
|
||||||
return dispatcher ?? const ActionDispatcher();
|
return dispatcher ?? const ActionDispatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a [VoidCallback] handler that invokes the bound action for the
|
||||||
|
/// given `intent` if the action is enabled, and returns null if the action is
|
||||||
|
/// not enabled.
|
||||||
|
///
|
||||||
|
/// This is intended to be used in widgets which have something similar to an
|
||||||
|
/// `onTap` handler, which takes a `VoidCallback`, and can be set to the
|
||||||
|
/// result of calling this function.
|
||||||
|
///
|
||||||
|
/// Creates a dependency on the [Actions] widget that maps the bound action so
|
||||||
|
/// that if the actions change, the context will be rebuilt and find the
|
||||||
|
/// updated action.
|
||||||
|
static VoidCallback handler<T extends Intent>(BuildContext context, T intent, {bool nullOk = false}) {
|
||||||
|
final Action<T> action = Actions.find<T>(context, nullOk: nullOk);
|
||||||
|
if (action != null && action.enabled) {
|
||||||
|
return () {
|
||||||
|
Actions.of(context).invokeAction(action, intent, context);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds the [Action] bound to the given intent type `T` in the given `context`.
|
||||||
|
///
|
||||||
|
/// Creates a dependency on the [Actions] widget that maps the bound action so
|
||||||
|
/// that if the actions change, the context will be rebuilt and find the
|
||||||
|
/// updated action.
|
||||||
|
static Action<T> find<T extends Intent>(BuildContext context, {bool nullOk = false}) {
|
||||||
|
Action<T> action;
|
||||||
|
|
||||||
|
_visitActionsAncestors(context, (InheritedElement element) {
|
||||||
|
final _ActionsMarker actions = element.widget as _ActionsMarker;
|
||||||
|
final Action<T> result = actions.actions[T] as Action<T>;
|
||||||
|
if (result != null) {
|
||||||
|
context.dependOnInheritedElement(element);
|
||||||
|
action = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(() {
|
||||||
|
if (nullOk) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (action == null) {
|
||||||
|
throw FlutterError('Unable to find an action for a $T in an $Actions widget '
|
||||||
|
'in the given context.\n'
|
||||||
|
"$Actions.find() was called on a context that doesn\'t contain an "
|
||||||
|
'$Actions widget with a mapping for the given intent type.\n'
|
||||||
|
'The context used was:\n'
|
||||||
|
' $context\n'
|
||||||
|
'The intent type requested was:\n'
|
||||||
|
' $T');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
/// 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].
|
||||||
///
|
///
|
||||||
@ -249,14 +543,13 @@ class Actions extends InheritedWidget {
|
|||||||
/// The `context` argument must not be null.
|
/// The `context` argument must not be null.
|
||||||
static ActionDispatcher of(BuildContext context, {bool nullOk = false}) {
|
static ActionDispatcher of(BuildContext context, {bool nullOk = false}) {
|
||||||
assert(context != null);
|
assert(context != null);
|
||||||
final InheritedElement inheritedElement = context.getElementForInheritedWidgetOfExactType<Actions>();
|
final _ActionsMarker marker = context.dependOnInheritedWidgetOfExactType<_ActionsMarker>();
|
||||||
final Actions inherited = context.dependOnInheritedElement(inheritedElement) as Actions;
|
|
||||||
assert(() {
|
assert(() {
|
||||||
if (nullOk) {
|
if (nullOk) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (inherited == null) {
|
if (marker == null) {
|
||||||
throw FlutterError('Unable to find an $Actions widget in the context.\n'
|
throw FlutterError('Unable to find an $Actions widget in the given context.\n'
|
||||||
'$Actions.of() was called with a context that does not contain an '
|
'$Actions.of() was called with a context that does not contain an '
|
||||||
'$Actions widget.\n'
|
'$Actions widget.\n'
|
||||||
'No $Actions ancestor could be found starting from the context that '
|
'No $Actions ancestor could be found starting from the context that '
|
||||||
@ -267,7 +560,7 @@ class Actions extends InheritedWidget {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
return inherited?.dispatcher ?? _findDispatcher(inheritedElement);
|
return marker?.dispatcher ?? _findDispatcher(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invokes the action associated with the given [Intent] using the
|
/// Invokes the action associated with the given [Intent] using the
|
||||||
@ -276,88 +569,173 @@ class Actions extends InheritedWidget {
|
|||||||
/// The `context`, `intent` and `nullOk` arguments must not be null.
|
/// The `context`, `intent` and `nullOk` arguments must not be null.
|
||||||
///
|
///
|
||||||
/// If the given `intent` isn't found in the first [Actions.actions] map, then
|
/// If the given `intent` isn't found in the first [Actions.actions] map, then
|
||||||
/// it will move up to the next [Actions] widget in the hierarchy until it
|
/// it will look to the next [Actions] widget in the hierarchy until it
|
||||||
/// reaches the root.
|
/// reaches the root.
|
||||||
///
|
///
|
||||||
/// Will throw if no ambient [Actions] widget is found, or if the given
|
/// Will throw if no ambient [Actions] widget is found, or if the given
|
||||||
/// `intent` doesn't map to an action in any of the [Actions.actions] maps
|
/// `intent` doesn't map to an action in any of the [Actions.actions] maps
|
||||||
/// that are found.
|
/// that are found.
|
||||||
///
|
///
|
||||||
/// Returns true if an action was successfully invoked.
|
|
||||||
///
|
|
||||||
/// Setting `nullOk` to true means that if no ambient [Actions] widget is
|
/// Setting `nullOk` to true means that if no ambient [Actions] widget is
|
||||||
/// found, then this method will return false instead of throwing.
|
/// found, then this method will return false instead of throwing.
|
||||||
static bool invoke(
|
static Object invoke<T extends Intent>(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
Intent intent, {
|
T intent, {
|
||||||
FocusNode focusNode,
|
|
||||||
bool nullOk = false,
|
bool nullOk = false,
|
||||||
}) {
|
}) {
|
||||||
assert(context != null);
|
|
||||||
assert(intent != null);
|
assert(intent != null);
|
||||||
Element actionsElement;
|
assert(nullOk != null);
|
||||||
Action action;
|
assert(context != null);
|
||||||
|
Action<T> action;
|
||||||
|
InheritedElement actionElement;
|
||||||
|
|
||||||
bool visitAncestorElement(Element element) {
|
_visitActionsAncestors(context, (InheritedElement element) {
|
||||||
if (element.widget is! Actions) {
|
final _ActionsMarker actions = element.widget as _ActionsMarker;
|
||||||
// Continue visiting.
|
final Action<T> result = actions.actions[intent.runtimeType] as Action<T>;
|
||||||
|
if (result != null) {
|
||||||
|
action = result;
|
||||||
|
actionElement = element;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Below when we invoke the action, we need to use the dispatcher from the
|
return false;
|
||||||
// Actions widget where we found the action, in case they need to match.
|
});
|
||||||
actionsElement = element;
|
|
||||||
final Actions actions = element.widget as Actions;
|
|
||||||
action = actions.actions[intent.key]?.call();
|
|
||||||
// Keep looking if we failed to find and create an action.
|
|
||||||
return action == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.visitAncestorElements(visitAncestorElement);
|
|
||||||
assert(() {
|
assert(() {
|
||||||
if (nullOk) {
|
if (nullOk) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (actionsElement == null) {
|
|
||||||
throw FlutterError('Unable to find a $Actions widget in the context.\n'
|
|
||||||
'$Actions.invoke() was called with a context that does not contain an '
|
|
||||||
'$Actions widget.\n'
|
|
||||||
'No $Actions ancestor could be found starting from the context that '
|
|
||||||
'was passed to $Actions.invoke(). This can happen if the context comes '
|
|
||||||
'from a widget above those widgets.\n'
|
|
||||||
'The context used was:\n'
|
|
||||||
' $context');
|
|
||||||
}
|
|
||||||
if (action == null) {
|
if (action == null) {
|
||||||
throw FlutterError('Unable to find an action for an intent in the $Actions widget in the context.\n'
|
throw FlutterError('Unable to find an action for an Intent with type '
|
||||||
"$Actions.invoke() was called on an $Actions widget that doesn't "
|
'${intent.runtimeType} in an $Actions widget in the given context.\n'
|
||||||
'contain a mapping for the given intent.\n'
|
'$Actions.invoke() was unable to find an $Actions widget that '
|
||||||
|
"contained a mapping for the given intent, or the intent type isn't the "
|
||||||
|
'same as the type argument to invoke (which is $T - try supplying a '
|
||||||
|
'type argument to invoke if one was not given)\n'
|
||||||
'The context used was:\n'
|
'The context used was:\n'
|
||||||
' $context\n'
|
' $context\n'
|
||||||
'The intent requested was:\n'
|
'The intent type requested was:\n'
|
||||||
' $intent');
|
' ${intent.runtimeType}');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
if (action == null) {
|
// Invoke the action we found using the relevant dispatcher from the Actions
|
||||||
// Will only get here if nullOk is true.
|
// Element we found.
|
||||||
return false;
|
return actionElement != null ? _findDispatcher(actionElement).invokeAction(action, intent, context) != null : null;
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke the action we found using the dispatcher from the Actions Element
|
|
||||||
// we found, using the given focus node.
|
|
||||||
return _findDispatcher(actionsElement).invokeAction(action, intent, focusNode: focusNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool updateShouldNotify(Actions oldWidget) {
|
State<Actions> createState() => _ActionsState();
|
||||||
return oldWidget.dispatcher != dispatcher || !mapEquals<LocalKey, ActionFactory>(oldWidget.actions, actions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
super.debugFillProperties(properties);
|
super.debugFillProperties(properties);
|
||||||
properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
|
properties.add(DiagnosticsProperty<ActionDispatcher>('dispatcher', dispatcher));
|
||||||
properties.add(DiagnosticsProperty<Map<LocalKey, ActionFactory>>('actions', actions));
|
properties.add(DiagnosticsProperty<Map<Type, Action<Intent>>>('actions', actions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ActionsState extends State<Actions> {
|
||||||
|
// Keeps the last-known enabled state of each action in the action map in
|
||||||
|
// order to be able to appropriately notify dependents that the state has
|
||||||
|
// changed.
|
||||||
|
Map<Action<Intent>, bool> enabledState = <Action<Intent>, bool>{};
|
||||||
|
// Used to tell the marker to rebuild when the enabled state of an action in
|
||||||
|
// the map changes.
|
||||||
|
Object rebuildKey = Object();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleActionChanged(Action<Intent> action) {
|
||||||
|
assert(enabledState.containsKey(action));
|
||||||
|
final bool actionEnabled = action.enabled;
|
||||||
|
if (enabledState[action] == null || enabledState[action] != actionEnabled) {
|
||||||
|
setState(() {
|
||||||
|
enabledState[action] = actionEnabled;
|
||||||
|
// Generate a new key so that the marker notifies dependents.
|
||||||
|
rebuildKey = Object();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateActionListeners() {
|
||||||
|
final Map<Action<Intent>, bool> newState = <Action<Intent>, bool>{};
|
||||||
|
final Set<Action<Intent>> foundActions = <Action<Intent>>{};
|
||||||
|
for (final Action<Intent> action in widget.actions.values) {
|
||||||
|
if (enabledState.containsKey(action)) {
|
||||||
|
// Already subscribed to this action, just copy over the current enabled state.
|
||||||
|
newState[action] = enabledState[action];
|
||||||
|
foundActions.add(action);
|
||||||
|
} else {
|
||||||
|
// New action to subscribe to.
|
||||||
|
// Don't set the new state to action.enabled, since that can cause
|
||||||
|
// problems when the enabled accessor looks up other widgets (which may
|
||||||
|
// have already been removed from the tree).
|
||||||
|
newState[action] = null;
|
||||||
|
action.addActionListener(_handleActionChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unregister from any actions in the previous enabledState map that aren't
|
||||||
|
// going to be transferred to the new one.
|
||||||
|
for (final Action<Intent> action in enabledState.keys) {
|
||||||
|
if (!foundActions.contains(action)) {
|
||||||
|
action.removeActionListener(_handleActionChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabledState = newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(Actions oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_updateActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
for (final Action<Intent> action in enabledState.keys) {
|
||||||
|
action.removeActionListener(_handleActionChanged);
|
||||||
|
}
|
||||||
|
enabledState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return _ActionsMarker(
|
||||||
|
actions: widget.actions,
|
||||||
|
dispatcher: widget.dispatcher,
|
||||||
|
rebuildKey: rebuildKey,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An inherited widget used by Actions widget for fast lookup of the Actions
|
||||||
|
// widget information.
|
||||||
|
class _ActionsMarker extends InheritedWidget {
|
||||||
|
const _ActionsMarker({
|
||||||
|
@required this.dispatcher,
|
||||||
|
@required this.actions,
|
||||||
|
@required this.rebuildKey,
|
||||||
|
Key key,
|
||||||
|
@required Widget child,
|
||||||
|
}) : assert(child != null),
|
||||||
|
assert(actions != null),
|
||||||
|
super(key: key, child: child);
|
||||||
|
|
||||||
|
final ActionDispatcher dispatcher;
|
||||||
|
final Map<Type, Action<Intent>> actions;
|
||||||
|
final Object rebuildKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(_ActionsMarker oldWidget) {
|
||||||
|
return rebuildKey != oldWidget.rebuildKey
|
||||||
|
|| oldWidget.dispatcher != dispatcher
|
||||||
|
|| !mapEquals<Type, Action<Intent>>(oldWidget.actions, actions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,22 +780,19 @@ class Actions extends InheritedWidget {
|
|||||||
/// bool _focused = false;
|
/// bool _focused = false;
|
||||||
/// bool _hovering = false;
|
/// bool _hovering = false;
|
||||||
/// bool _on = false;
|
/// bool _on = false;
|
||||||
/// Map<LocalKey, ActionFactory> _actionMap;
|
/// Map<Type, Action<Intent>> _actionMap;
|
||||||
/// Map<LogicalKeySet, Intent> _shortcutMap;
|
/// Map<LogicalKeySet, Intent> _shortcutMap;
|
||||||
///
|
///
|
||||||
/// @override
|
/// @override
|
||||||
/// void initState() {
|
/// void initState() {
|
||||||
/// super.initState();
|
/// super.initState();
|
||||||
/// _actionMap = <LocalKey, ActionFactory>{
|
/// _actionMap = <Type, Action<Intent>>{
|
||||||
/// ActivateAction.key: () {
|
/// ActivateIntent: CallbackAction(
|
||||||
/// return CallbackAction(
|
/// onInvoke: (Intent intent) => _toggleState(),
|
||||||
/// ActivateAction.key,
|
/// ),
|
||||||
/// onInvoke: (FocusNode node, Intent intent) => _toggleState(),
|
|
||||||
/// );
|
|
||||||
/// },
|
|
||||||
/// };
|
/// };
|
||||||
/// _shortcutMap = <LogicalKeySet, Intent>{
|
/// _shortcutMap = <LogicalKeySet, Intent>{
|
||||||
/// LogicalKeySet(LogicalKeyboardKey.keyX): Intent(ActivateAction.key),
|
/// LogicalKeySet(LogicalKeyboardKey.keyX): const ActivateIntent(),
|
||||||
/// };
|
/// };
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@ -546,7 +921,7 @@ class FocusableActionDetector extends StatefulWidget {
|
|||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.actions.actions}
|
/// {@macro flutter.widgets.actions.actions}
|
||||||
final Map<LocalKey, ActionFactory> actions;
|
final Map<Type, Action<Intent>> actions;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.shortcuts.shortcuts}
|
/// {@macro flutter.widgets.shortcuts.shortcuts}
|
||||||
final Map<LogicalKeySet, Intent> shortcuts;
|
final Map<LogicalKeySet, Intent> shortcuts;
|
||||||
@ -656,6 +1031,7 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
|
|||||||
bool shouldShowHoverHighlight(FocusableActionDetector target) {
|
bool shouldShowHoverHighlight(FocusableActionDetector target) {
|
||||||
return _hovering && target.enabled && _canShowHighlight;
|
return _hovering && target.enabled && _canShowHighlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldShowFocusHighlight(FocusableActionDetector target) {
|
bool shouldShowFocusHighlight(FocusableActionDetector target) {
|
||||||
return _focused && target.enabled && _canShowHighlight;
|
return _focused && target.enabled && _canShowHighlight;
|
||||||
}
|
}
|
||||||
@ -664,14 +1040,17 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
|
|||||||
final FocusableActionDetector oldTarget = oldWidget ?? widget;
|
final FocusableActionDetector oldTarget = oldWidget ?? widget;
|
||||||
final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
|
final bool didShowHoverHighlight = shouldShowHoverHighlight(oldTarget);
|
||||||
final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
|
final bool didShowFocusHighlight = shouldShowFocusHighlight(oldTarget);
|
||||||
if (task != null)
|
if (task != null) {
|
||||||
task();
|
task();
|
||||||
|
}
|
||||||
final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
|
final bool doShowHoverHighlight = shouldShowHoverHighlight(widget);
|
||||||
final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
|
final bool doShowFocusHighlight = shouldShowFocusHighlight(widget);
|
||||||
if (didShowFocusHighlight != doShowFocusHighlight)
|
if (didShowFocusHighlight != doShowFocusHighlight) {
|
||||||
widget.onShowFocusHighlight?.call(doShowFocusHighlight);
|
widget.onShowFocusHighlight?.call(doShowFocusHighlight);
|
||||||
if (didShowHoverHighlight != doShowHoverHighlight)
|
}
|
||||||
|
if (didShowHoverHighlight != doShowHoverHighlight) {
|
||||||
widget.onShowHoverHighlight?.call(doShowHoverHighlight);
|
widget.onShowHoverHighlight?.call(doShowHoverHighlight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -707,44 +1086,54 @@ class _FocusableActionDetectorState extends State<FocusableActionDetector> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An [Intent], that, as the name implies, is bound to a [DoNothingAction].
|
||||||
|
///
|
||||||
|
/// Attaching a [DoNothingIntent] to a [Shortcuts] mapping is one way to disable
|
||||||
|
/// a keyboard shortcut defined by a widget higher in the widget hierarchy.
|
||||||
|
///
|
||||||
|
/// This intent cannot be subclassed.
|
||||||
|
class DoNothingIntent extends Intent {
|
||||||
|
/// Creates a const [DoNothingIntent].
|
||||||
|
factory DoNothingIntent() => const DoNothingIntent._();
|
||||||
|
|
||||||
|
// Make DoNothingIntent constructor private so it can't be subclassed.
|
||||||
|
const DoNothingIntent._();
|
||||||
|
}
|
||||||
|
|
||||||
/// An [Action], that, as the name implies, does nothing.
|
/// An [Action], that, as the name implies, does nothing.
|
||||||
///
|
///
|
||||||
/// This action is bound to the [Intent.doNothing] intent inside of
|
/// Attaching a [DoNothingAction] to an [Actions] mapping is one way to disable
|
||||||
/// [WidgetsApp.build] so that a [Shortcuts] widget can bind a key to it to
|
/// an action defined by a widget higher in the widget hierarchy.
|
||||||
/// override another shortcut binding defined above it in the hierarchy.
|
///
|
||||||
class DoNothingAction extends Action {
|
/// This action can be bound to any intent.
|
||||||
/// Const constructor for [DoNothingAction].
|
///
|
||||||
const DoNothingAction() : super(key);
|
/// See also:
|
||||||
|
/// - [DoNothingIntent], which is an intent that can be bound to a keystroke in
|
||||||
/// The Key used for the [DoNothingIntent] intent, and registered at the top
|
/// a [Shortcuts] widget to do nothing.
|
||||||
/// level actions in [WidgetsApp.build].
|
class DoNothingAction extends Action<Intent> {
|
||||||
static const LocalKey key = ValueKey<Type>(DoNothingAction);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) { }
|
void invoke(Intent intent) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An action that invokes the currently focused control.
|
/// An intent that activates the currently focused control.
|
||||||
|
class ActivateIntent extends Intent {
|
||||||
|
/// Creates a const [ActivateIntent] so subclasses can be const.
|
||||||
|
const ActivateIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An action that activates the currently focused control.
|
||||||
///
|
///
|
||||||
/// This is an abstract class that serves as a base class for actions that
|
/// This is an abstract class that serves as a base class for actions that
|
||||||
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter] in
|
/// activate a control. By default, is bound to [LogicalKeyboardKey.enter],
|
||||||
/// the default keyboard map in [WidgetsApp].
|
/// [LogicalKeyboardKey.gameButtonA], and [LogicalKeyboardKey.space] in the
|
||||||
abstract class ActivateAction extends Action {
|
/// default keyboard map in [WidgetsApp].
|
||||||
/// Creates a [ActivateAction] with a fixed [key];
|
abstract class ActivateAction extends Action<ActivateIntent> {}
|
||||||
const ActivateAction() : super(key);
|
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely identifies this action.
|
/// An intent that selects the currently focused control.
|
||||||
static const LocalKey key = ValueKey<Type>(ActivateAction);
|
class SelectIntent extends Intent {}
|
||||||
}
|
|
||||||
|
|
||||||
/// An action that selects the currently focused control.
|
/// An action that selects the currently focused control.
|
||||||
///
|
///
|
||||||
/// This is an abstract class that serves as a base class for actions that
|
/// This is an abstract class that serves as a base class for actions that
|
||||||
/// select something. It is not bound to any key by default.
|
/// select something. It is not bound to any key by default.
|
||||||
abstract class SelectAction extends Action {
|
abstract class SelectAction extends Action<SelectIntent> {}
|
||||||
/// Creates a [SelectAction] with a fixed [key];
|
|
||||||
const SelectAction() : super(key);
|
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely identifies this action.
|
|
||||||
static const LocalKey key = ValueKey<Type>(SelectAction);
|
|
||||||
}
|
|
||||||
|
@ -743,7 +743,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// shortcuts: <LogicalKeySet, Intent>{
|
/// shortcuts: <LogicalKeySet, Intent>{
|
||||||
/// ... WidgetsApp.defaultShortcuts,
|
/// ... WidgetsApp.defaultShortcuts,
|
||||||
/// LogicalKeySet(LogicalKeyboardKey.select): const Intent(ActivateAction.key),
|
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||||
/// },
|
/// },
|
||||||
/// color: const Color(0xFFFF0000),
|
/// color: const Color(0xFFFF0000),
|
||||||
/// builder: (BuildContext context, Widget child) {
|
/// builder: (BuildContext context, Widget child) {
|
||||||
@ -790,12 +790,12 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
/// ```dart
|
/// ```dart
|
||||||
/// Widget build(BuildContext context) {
|
/// Widget build(BuildContext context) {
|
||||||
/// return WidgetsApp(
|
/// return WidgetsApp(
|
||||||
/// actions: <LocalKey, ActionFactory>{
|
/// actions: <Type, Action<Intent>>{
|
||||||
/// ... WidgetsApp.defaultActions,
|
/// ... WidgetsApp.defaultActions,
|
||||||
/// ActivateAction.key: () => CallbackAction(
|
/// ActivateAction: CallbackAction(
|
||||||
/// ActivateAction.key,
|
/// onInvoke: (Intent intent) {
|
||||||
/// onInvoke: (FocusNode focusNode, Intent intent) {
|
|
||||||
/// // Do something here...
|
/// // Do something here...
|
||||||
|
/// return null;
|
||||||
/// },
|
/// },
|
||||||
/// ),
|
/// ),
|
||||||
/// },
|
/// },
|
||||||
@ -818,7 +818,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
/// * The [Intent] and [Action] classes, which allow definition of new
|
/// * The [Intent] and [Action] classes, which allow definition of new
|
||||||
/// actions.
|
/// actions.
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final Map<LocalKey, ActionFactory> actions;
|
final Map<Type, Action<Intent>> actions;
|
||||||
|
|
||||||
/// If true, forces the performance overlay to be visible in all instances.
|
/// 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>{
|
static final Map<LogicalKeySet, Intent> _defaultShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// Activation
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.gameButtonA): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.gameButtonA): const ActivateIntent(),
|
||||||
|
|
||||||
// Keyboard traversal.
|
// Keyboard traversal.
|
||||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||||
@ -869,11 +869,11 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
// Default shortcuts for the web platform.
|
// Default shortcuts for the web platform.
|
||||||
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
static final Map<LogicalKeySet, Intent> _defaultWebShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// Activation
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
|
|
||||||
// Keyboard traversal.
|
// Keyboard traversal.
|
||||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
|
||||||
|
|
||||||
// Scrolling
|
// Scrolling
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
|
LogicalKeySet(LogicalKeyboardKey.arrowUp): const ScrollIntent(direction: AxisDirection.up),
|
||||||
@ -887,12 +887,12 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
// Default shortcuts for the macOS platform.
|
// Default shortcuts for the macOS platform.
|
||||||
static final Map<LogicalKeySet, Intent> _defaultMacOsShortcuts = <LogicalKeySet, Intent>{
|
static final Map<LogicalKeySet, Intent> _defaultMacOsShortcuts = <LogicalKeySet, Intent>{
|
||||||
// Activation
|
// Activation
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
|
|
||||||
// Keyboard traversal
|
// Keyboard traversal
|
||||||
LogicalKeySet(LogicalKeyboardKey.tab): const Intent(NextFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.tab): const NextFocusIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const Intent(PreviousFocusAction.key),
|
LogicalKeySet(LogicalKeyboardKey.shift, LogicalKeyboardKey.tab): const PreviousFocusIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const DirectionalFocusIntent(TraversalDirection.left),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
LogicalKeySet(LogicalKeyboardKey.arrowRight): const DirectionalFocusIntent(TraversalDirection.right),
|
||||||
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
LogicalKeySet(LogicalKeyboardKey.arrowDown): const DirectionalFocusIntent(TraversalDirection.down),
|
||||||
@ -932,13 +932,13 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The default value of [WidgetsApp.actions].
|
/// The default value of [WidgetsApp.actions].
|
||||||
static final Map<LocalKey, ActionFactory> defaultActions = <LocalKey, ActionFactory>{
|
static Map<Type, Action<Intent>> defaultActions = <Type, Action<Intent>>{
|
||||||
DoNothingAction.key: () => const DoNothingAction(),
|
DoNothingIntent: DoNothingAction(),
|
||||||
RequestFocusAction.key: () => RequestFocusAction(),
|
RequestFocusIntent: RequestFocusAction(),
|
||||||
NextFocusAction.key: () => NextFocusAction(),
|
NextFocusIntent: NextFocusAction(),
|
||||||
PreviousFocusAction.key: () => PreviousFocusAction(),
|
PreviousFocusIntent: PreviousFocusAction(),
|
||||||
DirectionalFocusAction.key: () => DirectionalFocusAction(),
|
DirectionalFocusIntent: DirectionalFocusAction(),
|
||||||
ScrollAction.key: () => ScrollAction(),
|
ScrollIntent: ScrollAction(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -92,7 +92,8 @@ enum TraversalDirection {
|
|||||||
/// [FocusTraversalGroup] widget.
|
/// [FocusTraversalGroup] widget.
|
||||||
///
|
///
|
||||||
/// The focus traversal policy is what determines which widget is "next",
|
/// 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
|
/// One of the pre-defined subclasses may be used, or define a custom policy to
|
||||||
/// create a unique focus order.
|
/// create a unique focus order.
|
||||||
@ -1713,88 +1714,94 @@ class _FocusTraversalGroupMarker extends InheritedWidget {
|
|||||||
bool updateShouldNotify(InheritedWidget oldWidget) => false;
|
bool updateShouldNotify(InheritedWidget oldWidget) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A base class for all of the default actions that request focus for a node.
|
/// An intent for use with the [RequestFocusAction], which supplies the
|
||||||
class _RequestFocusActionBase extends Action {
|
/// [FocusNode] that should be focused.
|
||||||
_RequestFocusActionBase(LocalKey name) : super(name);
|
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;
|
/// The [FocusNode] that is to be focused.
|
||||||
|
final FocusNode focusNode;
|
||||||
@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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
/// This action can be used to request focus for a particular node, by calling
|
||||||
/// [Action.invoke] like so:
|
/// [Action.invoke] like so:
|
||||||
///
|
///
|
||||||
/// ```dart
|
/// ```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
|
/// The difference between requesting focus in this way versus calling
|
||||||
/// [_focusNode.requestFocus] directly is that it will use the [Action]
|
/// [FocusNode.requestFocus] directly is that it will use the [Action]
|
||||||
/// registered in the nearest [Actions] widget associated with [key] to make the
|
/// registered in the nearest [Actions] widget associated with
|
||||||
/// request, rather than just requesting focus directly. This allows the action
|
/// [RequestFocusIntent] to make the request, rather than just requesting focus
|
||||||
/// to have additional side effects, like logging, or undo and redo
|
/// directly. This allows the action to have additional side effects, like
|
||||||
/// functionality.
|
/// logging, or undo and redo functionality.
|
||||||
///
|
///
|
||||||
/// However, this [RequestFocusAction] is the default action associated with the
|
/// This [RequestFocusAction] class is the default action associated with the
|
||||||
/// [key] in the [WidgetsApp], and it simply requests focus and has no side
|
/// [RequestFocusIntent] in the [WidgetsApp], and it simply requests focus. You
|
||||||
/// effects.
|
/// can redefine the associated action with your own [Actions] widget.
|
||||||
class RequestFocusAction extends _RequestFocusActionBase {
|
///
|
||||||
/// Creates a [RequestFocusAction] with a fixed [key].
|
/// See [FocusTraversalPolicy] for more information about focus traversal.
|
||||||
RequestFocusAction() : super(key);
|
class RequestFocusAction extends Action<RequestFocusIntent> {
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
|
||||||
static const LocalKey key = ValueKey<Type>(RequestFocusAction);
|
|
||||||
|
|
||||||
@override
|
@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
|
/// An [Action] that moves the focus to the next focusable node in the focus
|
||||||
/// order.
|
/// order.
|
||||||
///
|
///
|
||||||
/// This action is the default action registered for the [key], and by default
|
/// This action is the default action registered for the [NextFocusIntent], and
|
||||||
/// is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
|
/// by default is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
|
||||||
class NextFocusAction extends _RequestFocusActionBase {
|
///
|
||||||
/// Creates a [NextFocusAction] with a fixed [key];
|
/// See [FocusTraversalPolicy] for more information about focus traversal.
|
||||||
NextFocusAction() : super(key);
|
class NextFocusAction extends Action<NextFocusIntent> {
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
|
||||||
static const LocalKey key = ValueKey<Type>(NextFocusAction);
|
|
||||||
|
|
||||||
@override
|
@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
|
/// An [Action] that moves the focus to the previous focusable node in the focus
|
||||||
/// order.
|
/// order.
|
||||||
///
|
///
|
||||||
/// This action is the default action registered for the [key], and by default
|
/// This action is the default action registered for the [PreviousFocusIntent],
|
||||||
/// is bound to a combination of the [LogicalKeyboardKey.tab] key and the
|
/// and by default is bound to a combination of the [LogicalKeyboardKey.tab] key
|
||||||
/// [LogicalKeyboardKey.shift] key in the [WidgetsApp].
|
/// and the [LogicalKeyboardKey.shift] key in the [WidgetsApp].
|
||||||
class PreviousFocusAction extends _RequestFocusActionBase {
|
///
|
||||||
/// Creates a [PreviousFocusAction] with a fixed [key];
|
/// See [FocusTraversalPolicy] for more information about focus traversal.
|
||||||
PreviousFocusAction() : super(key);
|
class PreviousFocusAction extends Action<PreviousFocusIntent> {
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely identifies this action to an [Intent].
|
|
||||||
static const LocalKey key = ValueKey<Type>(PreviousFocusAction);
|
|
||||||
|
|
||||||
@override
|
@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
|
/// 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.arrowDown], [LogicalKeyboardKey.arrowLeft], and
|
||||||
/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
|
/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
|
||||||
/// appropriate associated directions.
|
/// appropriate associated directions.
|
||||||
|
///
|
||||||
|
/// See [FocusTraversalPolicy] for more information about focus traversal.
|
||||||
class DirectionalFocusIntent extends Intent {
|
class DirectionalFocusIntent extends Intent {
|
||||||
/// Creates a [DirectionalFocusIntent] with a fixed [key], and the given
|
/// Creates a [DirectionalFocusIntent] intending to move the focus in the
|
||||||
/// [direction].
|
/// given [direction].
|
||||||
const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true})
|
const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true})
|
||||||
: assert(ignoreTextFields != null),
|
: assert(ignoreTextFields != null);
|
||||||
super(DirectionalFocusAction.key);
|
|
||||||
|
|
||||||
/// The direction in which to look for the next focusable node when the
|
/// The direction in which to look for the next focusable node when the
|
||||||
/// associated [DirectionalFocusAction] is invoked.
|
/// 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
|
/// An [Action] that moves the focus to the focusable node in the direction
|
||||||
/// configured by the associated [DirectionalFocusIntent.direction].
|
/// configured by the associated [DirectionalFocusIntent.direction].
|
||||||
///
|
///
|
||||||
/// This is the [Action] associated with the [key] and bound by default to the
|
/// This is the [Action] associated with [DirectionalFocusIntent] and bound by
|
||||||
/// [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
|
/// default to the [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
|
||||||
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
|
/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
|
||||||
/// the [WidgetsApp], with the appropriate associated directions.
|
/// the [WidgetsApp], with the appropriate associated directions.
|
||||||
class DirectionalFocusAction extends _RequestFocusActionBase {
|
class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
|
||||||
/// 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);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, DirectionalFocusIntent intent) {
|
void invoke(DirectionalFocusIntent intent) {
|
||||||
if (!intent.ignoreTextFields || node.context.widget is! EditableText) {
|
if (!intent.ignoreTextFields || primaryFocus.context.widget is! EditableText) {
|
||||||
node.focusInDirection(intent.direction);
|
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 Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
|
||||||
static final Set<Element> _debugIllFatedElements = HashSet<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.
|
// Parent, child -> global key.
|
||||||
// This provides us a way to remove old reservation while parent rebuilds the
|
// This provides us a way to remove old reservation while parent rebuilds the
|
||||||
// child in the same slot.
|
// child in the same slot.
|
||||||
|
@ -889,8 +889,7 @@ class ScrollIntent extends Intent {
|
|||||||
@required this.direction,
|
@required this.direction,
|
||||||
this.type = ScrollIncrementType.line,
|
this.type = ScrollIncrementType.line,
|
||||||
}) : assert(direction != null),
|
}) : assert(direction != null),
|
||||||
assert(type != null),
|
assert(type != null);
|
||||||
super(ScrollAction.key);
|
|
||||||
|
|
||||||
/// The direction in which to scroll the scrollable containing the focused
|
/// The direction in which to scroll the scrollable containing the focused
|
||||||
/// widget.
|
/// widget.
|
||||||
@ -898,11 +897,6 @@ class ScrollIntent extends Intent {
|
|||||||
|
|
||||||
/// The type of scrolling that is intended.
|
/// The type of scrolling that is intended.
|
||||||
final ScrollIncrementType type;
|
final ScrollIncrementType type;
|
||||||
|
|
||||||
@override
|
|
||||||
bool isEnabled(BuildContext context) {
|
|
||||||
return Scrollable.of(context) != null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An [Action] that scrolls the [Scrollable] that encloses the current
|
/// 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
|
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
|
||||||
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
|
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
|
||||||
/// pixels.
|
/// pixels.
|
||||||
class ScrollAction extends Action {
|
class ScrollAction extends Action<ScrollIntent> {
|
||||||
/// Creates a const [ScrollAction].
|
|
||||||
ScrollAction() : super(key);
|
|
||||||
|
|
||||||
/// The [LocalKey] that uniquely connects this action to a [ScrollIntent].
|
/// The [LocalKey] that uniquely connects this action to a [ScrollIntent].
|
||||||
static const LocalKey key = ValueKey<Type>(ScrollAction);
|
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
|
// Returns the scroll increment for a single scroll request, for use when
|
||||||
// scrolling using a hardware keyboard.
|
// scrolling using a hardware keyboard.
|
||||||
//
|
//
|
||||||
@ -1013,8 +1010,8 @@ class ScrollAction extends Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, ScrollIntent intent) {
|
void invoke(ScrollIntent intent) {
|
||||||
final ScrollableState state = Scrollable.of(node.context);
|
final ScrollableState state = Scrollable.of(primaryFocus.context);
|
||||||
assert(state != null, '$ScrollAction was invoked on a context that has no scrollable parent');
|
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.pixels != null, 'Scrollable must be laid out before it can be scrolled via a ScrollAction');
|
||||||
assert(state.position.viewportDimension != null);
|
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
|
/// The optional `keysPressed` argument provides an override to keys that the
|
||||||
/// [RawKeyboard] reports. If not specified, uses [RawKeyboard.keysPressed]
|
/// [RawKeyboard] reports. If not specified, uses [RawKeyboard.keysPressed]
|
||||||
/// instead.
|
/// 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
|
@protected
|
||||||
bool handleKeypress(
|
bool handleKeypress(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -316,10 +324,9 @@ class ShortcutManager extends ChangeNotifier with Diagnosticable {
|
|||||||
}
|
}
|
||||||
if (matchedIntent != null) {
|
if (matchedIntent != null) {
|
||||||
final BuildContext primaryContext = primaryFocus?.context;
|
final BuildContext primaryContext = primaryFocus?.context;
|
||||||
if (primaryContext == null) {
|
assert (primaryContext != null);
|
||||||
return false;
|
Actions.invoke(primaryContext, matchedIntent, nullOk: true);
|
||||||
}
|
return true;
|
||||||
return Actions.invoke(primaryContext, matchedIntent, nullOk: true);
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -174,6 +174,7 @@ void main() {
|
|||||||
' Focus\n'
|
' Focus\n'
|
||||||
' _FocusTraversalGroupMarker\n'
|
' _FocusTraversalGroupMarker\n'
|
||||||
' FocusTraversalGroup\n'
|
' FocusTraversalGroup\n'
|
||||||
|
' _ActionsMarker\n'
|
||||||
' Actions\n'
|
' Actions\n'
|
||||||
' _ShortcutsMarker\n'
|
' _ShortcutsMarker\n'
|
||||||
' Semantics\n'
|
' Semantics\n'
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
@ -259,11 +258,11 @@ void main() {
|
|||||||
final BorderRadius borderRadius = BorderRadius.circular(6.0);
|
final BorderRadius borderRadius = BorderRadius.circular(6.0);
|
||||||
|
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
||||||
Future<void> buildTest(LocalKey actionKey) async {
|
Future<void> buildTest(Intent intent) async {
|
||||||
return await tester.pumpWidget(
|
return await tester.pumpWidget(
|
||||||
Shortcuts(
|
Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): Intent(actionKey),
|
LogicalKeySet(LogicalKeyboardKey.space): intent,
|
||||||
},
|
},
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
@ -289,7 +288,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await buildTest(ActivateAction.key);
|
await buildTest(const ActivateIntent());
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
@ -322,7 +321,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await buildTest(ActivateAction.key);
|
await buildTest(const ActivateIntent());
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
@ -323,7 +323,7 @@ void main() {
|
|||||||
semantics.dispose();
|
semantics.dispose();
|
||||||
});
|
});
|
||||||
testWidgets("ink response doesn't focus when disabled", (WidgetTester tester) async {
|
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 FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
|
||||||
final GlobalKey childKey = GlobalKey();
|
final GlobalKey childKey = GlobalKey();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@ -359,7 +359,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets("ink response doesn't hover when disabled", (WidgetTester tester) async {
|
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 FocusNode focusNode = FocusNode(debugLabel: 'Ink Focus');
|
||||||
final GlobalKey childKey = GlobalKey();
|
final GlobalKey childKey = GlobalKey();
|
||||||
bool hovering = false;
|
bool hovering = false;
|
||||||
|
@ -47,8 +47,8 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Shortcuts(
|
Shortcuts(
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.enter): const ActivateIntent(),
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(ActivateAction.key),
|
LogicalKeySet(LogicalKeyboardKey.space): const ActivateIntent(),
|
||||||
},
|
},
|
||||||
child: Directionality(
|
child: Directionality(
|
||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
|
@ -9,17 +9,51 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.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, ActionDispatcher dispatcher});
|
||||||
|
|
||||||
class TestAction extends CallbackAction {
|
class TestIntent extends Intent {
|
||||||
const TestAction({
|
const TestIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecondTestIntent extends TestIntent {
|
||||||
|
const SecondTestIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ThirdTestIntent extends SecondTestIntent {
|
||||||
|
const ThirdTestIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestAction extends CallbackAction<TestIntent> {
|
||||||
|
TestAction({
|
||||||
@required OnInvokeCallback onInvoke,
|
@required OnInvokeCallback onInvoke,
|
||||||
}) : assert(onInvoke != null),
|
}) : assert(onInvoke != null),
|
||||||
super(key, onInvoke: onInvoke);
|
super(onInvoke: onInvoke);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(TestAction);
|
@override
|
||||||
|
bool get enabled => _enabled;
|
||||||
|
bool _enabled = true;
|
||||||
|
set enabled(bool value) {
|
||||||
|
if (_enabled == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_enabled = value;
|
||||||
|
notifyActionListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void _testInvoke(FocusNode node, Intent invocation) => invoke(node, invocation);
|
@override
|
||||||
|
void addActionListener(ActionListenerCallback listener) {
|
||||||
|
super.addActionListener(listener);
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void removeActionListener(ActionListenerCallback listener) {
|
||||||
|
super.removeActionListener(listener);
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
List<ActionListenerCallback> listeners = <ActionListenerCallback>[];
|
||||||
|
|
||||||
|
void _testInvoke(TestIntent intent) => invoke(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestDispatcher extends ActionDispatcher {
|
class TestDispatcher extends ActionDispatcher {
|
||||||
@ -28,9 +62,9 @@ class TestDispatcher extends ActionDispatcher {
|
|||||||
final PostInvokeCallback postInvoke;
|
final PostInvokeCallback postInvoke;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
|
Object invokeAction(Action<Intent> action, Intent intent, [BuildContext context]) {
|
||||||
final bool result = super.invokeAction(action, intent, focusNode: focusNode);
|
final Object result = super.invokeAction(action, intent, context);
|
||||||
postInvoke?.call(action: action, intent: intent, focusNode: focusNode, dispatcher: this);
|
postInvoke?.call(action: action, intent: intent, dispatcher: this);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,57 +74,48 @@ class TestDispatcher1 extends TestDispatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('Action passes parameters on when invoked.', () {
|
testWidgets('CallbackAction passes correct intent when invoked.', (WidgetTester tester) async {
|
||||||
bool invoked = false;
|
Intent passedIntent;
|
||||||
FocusNode passedNode;
|
final TestAction action = TestAction(onInvoke: (Intent intent) {
|
||||||
final TestAction action = TestAction(onInvoke: (FocusNode node, Intent invocation) {
|
passedIntent = intent;
|
||||||
invoked = true;
|
return true;
|
||||||
passedNode = node;
|
|
||||||
});
|
});
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
const TestIntent intent = TestIntent();
|
||||||
action._testInvoke(testNode, null);
|
action._testInvoke(intent);
|
||||||
expect(passedNode, equals(testNode));
|
expect(passedIntent, equals(intent));
|
||||||
expect(action.intentKey, equals(TestAction.key));
|
|
||||||
expect(invoked, isTrue);
|
|
||||||
});
|
});
|
||||||
group(ActionDispatcher, () {
|
group(ActionDispatcher, () {
|
||||||
test('ActionDispatcher invokes actions when asked.', () {
|
testWidgets('ActionDispatcher invokes actions when asked.', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(Container());
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
FocusNode passedNode;
|
|
||||||
const ActionDispatcher dispatcher = ActionDispatcher();
|
const ActionDispatcher dispatcher = ActionDispatcher();
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
final Object result = dispatcher.invokeAction(
|
||||||
final bool result = dispatcher.invokeAction(
|
|
||||||
TestAction(
|
TestAction(
|
||||||
onInvoke: (FocusNode node, Intent invocation) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
passedNode = node;
|
return invoked;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Intent(TestAction.key),
|
const TestIntent(),
|
||||||
focusNode: testNode,
|
|
||||||
);
|
);
|
||||||
expect(passedNode, equals(testNode));
|
|
||||||
expect(result, isTrue);
|
expect(result, isTrue);
|
||||||
expect(invoked, isTrue);
|
expect(invoked, isTrue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
group(Actions, () {
|
group(Actions, () {
|
||||||
Intent invokedIntent;
|
Intent invokedIntent;
|
||||||
Action invokedAction;
|
Action<Intent> invokedAction;
|
||||||
FocusNode invokedNode;
|
|
||||||
ActionDispatcher invokedDispatcher;
|
ActionDispatcher invokedDispatcher;
|
||||||
|
|
||||||
void collect({Action action, Intent intent, FocusNode focusNode, ActionDispatcher dispatcher}) {
|
void collect({Action<Intent> action, Intent intent, ActionDispatcher dispatcher}) {
|
||||||
invokedIntent = intent;
|
invokedIntent = intent;
|
||||||
invokedAction = action;
|
invokedAction = action;
|
||||||
invokedNode = focusNode;
|
|
||||||
invokedDispatcher = dispatcher;
|
invokedDispatcher = dispatcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
invokedIntent = null;
|
invokedIntent = null;
|
||||||
invokedAction = null;
|
invokedAction = null;
|
||||||
invokedNode = null;
|
|
||||||
invokedDispatcher = null;
|
invokedDispatcher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,64 +124,55 @@ void main() {
|
|||||||
testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
|
testWidgets('Actions widget can invoke actions with default dispatcher', (WidgetTester tester) async {
|
||||||
final GlobalKey containerKey = GlobalKey();
|
final GlobalKey containerKey = GlobalKey();
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
FocusNode passedNode;
|
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => TestAction(
|
TestIntent: TestAction(
|
||||||
onInvoke: (FocusNode node, Intent invocation) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
passedNode = node;
|
return invoked;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
child: Container(key: containerKey),
|
child: Container(key: containerKey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
final bool result = Actions.invoke(
|
final Object result = Actions.invoke(
|
||||||
containerKey.currentContext,
|
containerKey.currentContext,
|
||||||
const Intent(TestAction.key),
|
const TestIntent(),
|
||||||
focusNode: testNode,
|
|
||||||
);
|
);
|
||||||
expect(passedNode, equals(testNode));
|
|
||||||
expect(result, isTrue);
|
expect(result, isTrue);
|
||||||
expect(invoked, isTrue);
|
expect(invoked, isTrue);
|
||||||
});
|
});
|
||||||
testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
|
testWidgets('Actions widget can invoke actions with custom dispatcher', (WidgetTester tester) async {
|
||||||
final GlobalKey containerKey = GlobalKey();
|
final GlobalKey containerKey = GlobalKey();
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
const Intent intent = Intent(TestAction.key);
|
const TestIntent intent = TestIntent();
|
||||||
FocusNode passedNode;
|
final Action<Intent> testAction = TestAction(
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
onInvoke: (Intent intent) {
|
||||||
final Action testAction = TestAction(
|
|
||||||
onInvoke: (FocusNode node, Intent intent) {
|
|
||||||
invoked = true;
|
invoked = true;
|
||||||
passedNode = node;
|
return invoked;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
dispatcher: TestDispatcher(postInvoke: collect),
|
dispatcher: TestDispatcher(postInvoke: collect),
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => testAction,
|
TestIntent: testAction,
|
||||||
},
|
},
|
||||||
child: Container(key: containerKey),
|
child: Container(key: containerKey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
final bool result = Actions.invoke(
|
final Object result = Actions.invoke<TestIntent>(
|
||||||
containerKey.currentContext,
|
containerKey.currentContext,
|
||||||
intent,
|
intent,
|
||||||
focusNode: testNode,
|
|
||||||
);
|
);
|
||||||
expect(passedNode, equals(testNode));
|
|
||||||
expect(invokedNode, equals(testNode));
|
|
||||||
expect(result, isTrue);
|
expect(result, isTrue);
|
||||||
expect(invoked, isTrue);
|
expect(invoked, isTrue);
|
||||||
expect(invokedIntent, equals(intent));
|
expect(invokedIntent, equals(intent));
|
||||||
@ -164,38 +180,33 @@ void main() {
|
|||||||
testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
|
testWidgets('Actions can invoke actions in ancestor dispatcher', (WidgetTester tester) async {
|
||||||
final GlobalKey containerKey = GlobalKey();
|
final GlobalKey containerKey = GlobalKey();
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
const Intent intent = Intent(TestAction.key);
|
const TestIntent intent = TestIntent();
|
||||||
FocusNode passedNode;
|
final Action<Intent> testAction = TestAction(
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
onInvoke: (Intent intent) {
|
||||||
final Action testAction = TestAction(
|
|
||||||
onInvoke: (FocusNode node, Intent invocation) {
|
|
||||||
invoked = true;
|
invoked = true;
|
||||||
passedNode = node;
|
return invoked;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
dispatcher: TestDispatcher1(postInvoke: collect),
|
dispatcher: TestDispatcher1(postInvoke: collect),
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => testAction,
|
TestIntent: testAction,
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
dispatcher: TestDispatcher(postInvoke: collect),
|
dispatcher: TestDispatcher(postInvoke: collect),
|
||||||
actions: const <LocalKey, ActionFactory>{},
|
actions: const <Type, Action<Intent>>{},
|
||||||
child: Container(key: containerKey),
|
child: Container(key: containerKey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
final bool result = Actions.invoke(
|
final Object result = Actions.invoke<TestIntent>(
|
||||||
containerKey.currentContext,
|
containerKey.currentContext,
|
||||||
intent,
|
intent,
|
||||||
focusNode: testNode,
|
|
||||||
);
|
);
|
||||||
expect(passedNode, equals(testNode));
|
|
||||||
expect(invokedNode, equals(testNode));
|
|
||||||
expect(result, isTrue);
|
expect(result, isTrue);
|
||||||
expect(invoked, isTrue);
|
expect(invoked, isTrue);
|
||||||
expect(invokedIntent, equals(intent));
|
expect(invokedIntent, equals(intent));
|
||||||
@ -205,37 +216,32 @@ void main() {
|
|||||||
testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
|
testWidgets("Actions can invoke actions in ancestor dispatcher if a lower one isn't specified", (WidgetTester tester) async {
|
||||||
final GlobalKey containerKey = GlobalKey();
|
final GlobalKey containerKey = GlobalKey();
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
const Intent intent = Intent(TestAction.key);
|
const TestIntent intent = TestIntent();
|
||||||
FocusNode passedNode;
|
final Action<Intent> testAction = TestAction(
|
||||||
final FocusNode testNode = FocusNode(debugLabel: 'Test Node');
|
onInvoke: (Intent intent) {
|
||||||
final Action testAction = TestAction(
|
|
||||||
onInvoke: (FocusNode node, Intent invocation) {
|
|
||||||
invoked = true;
|
invoked = true;
|
||||||
passedNode = node;
|
return invoked;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
dispatcher: TestDispatcher1(postInvoke: collect),
|
dispatcher: TestDispatcher1(postInvoke: collect),
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => testAction,
|
TestIntent: testAction,
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: const <LocalKey, ActionFactory>{},
|
actions: const <Type, Action<Intent>>{},
|
||||||
child: Container(key: containerKey),
|
child: Container(key: containerKey),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
final bool result = Actions.invoke(
|
final Object result = Actions.invoke<TestIntent>(
|
||||||
containerKey.currentContext,
|
containerKey.currentContext,
|
||||||
intent,
|
intent,
|
||||||
focusNode: testNode,
|
|
||||||
);
|
);
|
||||||
expect(passedNode, equals(testNode));
|
|
||||||
expect(invokedNode, equals(testNode));
|
|
||||||
expect(result, isTrue);
|
expect(result, isTrue);
|
||||||
expect(invoked, isTrue);
|
expect(invoked, isTrue);
|
||||||
expect(invokedIntent, equals(intent));
|
expect(invokedIntent, equals(intent));
|
||||||
@ -249,7 +255,7 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
dispatcher: testDispatcher,
|
dispatcher: testDispatcher,
|
||||||
actions: const <LocalKey, ActionFactory>{},
|
actions: const <Type, Action<Intent>>{},
|
||||||
child: Container(key: containerKey),
|
child: Container(key: containerKey),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -261,15 +267,64 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(dispatcher, equals(testDispatcher));
|
expect(dispatcher, equals(testDispatcher));
|
||||||
});
|
});
|
||||||
|
testWidgets('Action can be found with find', (WidgetTester tester) async {
|
||||||
|
final GlobalKey containerKey = GlobalKey();
|
||||||
|
final ActionDispatcher testDispatcher = TestDispatcher1(postInvoke: collect);
|
||||||
|
bool invoked = false;
|
||||||
|
final TestAction testAction = TestAction(
|
||||||
|
onInvoke: (Intent intent) {
|
||||||
|
invoked = true;
|
||||||
|
return invoked;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
dispatcher: testDispatcher,
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: testAction,
|
||||||
|
},
|
||||||
|
child: Actions(
|
||||||
|
actions: const <Type, Action<Intent>>{},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(Actions.find<TestIntent>(containerKey.currentContext), equals(testAction));
|
||||||
|
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError);
|
||||||
|
expect(Actions.find<DoNothingIntent>(containerKey.currentContext, nullOk: true), isNull);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
dispatcher: testDispatcher,
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: testAction,
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
child: Actions(
|
||||||
|
actions: const <Type, Action<Intent>>{},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
expect(Actions.find<TestIntent>(containerKey.currentContext), equals(testAction));
|
||||||
|
expect(() => Actions.find<DoNothingIntent>(containerKey.currentContext), throwsAssertionError);
|
||||||
|
expect(Actions.find<DoNothingIntent>(containerKey.currentContext, nullOk: true), isNull);
|
||||||
|
});
|
||||||
testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
|
testWidgets('FocusableActionDetector keeps track of focus and hover even when disabled.', (WidgetTester tester) async {
|
||||||
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||||
final GlobalKey containerKey = GlobalKey();
|
final GlobalKey containerKey = GlobalKey();
|
||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
const Intent intent = Intent(TestAction.key);
|
const Intent intent = TestIntent();
|
||||||
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
|
||||||
final Action testAction = TestAction(
|
final Action<Intent> testAction = TestAction(
|
||||||
onInvoke: (FocusNode node, Intent invocation) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
|
return invoked;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
bool hovering = false;
|
bool hovering = false;
|
||||||
@ -280,15 +335,15 @@ void main() {
|
|||||||
Center(
|
Center(
|
||||||
child: Actions(
|
child: Actions(
|
||||||
dispatcher: TestDispatcher1(postInvoke: collect),
|
dispatcher: TestDispatcher1(postInvoke: collect),
|
||||||
actions: const <LocalKey, ActionFactory>{},
|
actions: const <Type, Action<Intent>>{},
|
||||||
child: FocusableActionDetector(
|
child: FocusableActionDetector(
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
shortcuts: <LogicalKeySet, Intent>{
|
shortcuts: <LogicalKeySet, Intent>{
|
||||||
LogicalKeySet(LogicalKeyboardKey.enter): intent,
|
LogicalKeySet(LogicalKeyboardKey.enter): intent,
|
||||||
},
|
},
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => testAction,
|
TestIntent: testAction,
|
||||||
},
|
},
|
||||||
onShowHoverHighlight: (bool value) => hovering = value,
|
onShowHoverHighlight: (bool value) => hovering = value,
|
||||||
onShowFocusHighlight: (bool value) => focusing = value,
|
onShowFocusHighlight: (bool value) => focusing = value,
|
||||||
@ -299,6 +354,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
return tester.pump();
|
return tester.pump();
|
||||||
}
|
}
|
||||||
|
|
||||||
await buildTest(true);
|
await buildTest(true);
|
||||||
focusNode.requestFocus();
|
focusNode.requestFocus();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -330,11 +386,178 @@ void main() {
|
|||||||
expect(focusing, isFalse);
|
expect(focusing, isFalse);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Listening', () {
|
||||||
|
testWidgets('can listen to enabled state of Actions', (WidgetTester tester) async {
|
||||||
|
final GlobalKey containerKey = GlobalKey();
|
||||||
|
bool invoked1 = false;
|
||||||
|
bool invoked2 = false;
|
||||||
|
bool invoked3 = false;
|
||||||
|
final TestAction action1 = TestAction(
|
||||||
|
onInvoke: (Intent intent) {
|
||||||
|
invoked1 = true;
|
||||||
|
return invoked1;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final TestAction action2 = TestAction(
|
||||||
|
onInvoke: (Intent intent) {
|
||||||
|
invoked2 = true;
|
||||||
|
return invoked2;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final TestAction action3 = TestAction(
|
||||||
|
onInvoke: (Intent intent) {
|
||||||
|
invoked3 = true;
|
||||||
|
return invoked3;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
bool enabled1 = true;
|
||||||
|
action1.addActionListener((Action<Intent> action) => enabled1 = action.enabled);
|
||||||
|
action1.enabled = false;
|
||||||
|
expect(enabled1, isFalse);
|
||||||
|
|
||||||
|
bool enabled2 = true;
|
||||||
|
action2.addActionListener((Action<Intent> action) => enabled2 = action.enabled);
|
||||||
|
action2.enabled = false;
|
||||||
|
expect(enabled2, isFalse);
|
||||||
|
|
||||||
|
bool enabled3 = true;
|
||||||
|
action3.addActionListener((Action<Intent> action) => enabled3 = action.enabled);
|
||||||
|
action3.enabled = false;
|
||||||
|
expect(enabled3, isFalse);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
actions: <Type, Action<TestIntent>>{
|
||||||
|
TestIntent: action1,
|
||||||
|
SecondTestIntent: action2,
|
||||||
|
},
|
||||||
|
child: Actions(
|
||||||
|
actions: <Type, Action<TestIntent>>{
|
||||||
|
ThirdTestIntent: action3,
|
||||||
|
},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Object result = Actions.invoke(
|
||||||
|
containerKey.currentContext,
|
||||||
|
const TestIntent(),
|
||||||
|
);
|
||||||
|
expect(enabled1, isFalse);
|
||||||
|
expect(result, isFalse);
|
||||||
|
expect(invoked1, isFalse);
|
||||||
|
|
||||||
|
action1.enabled = true;
|
||||||
|
result = Actions.invoke(
|
||||||
|
containerKey.currentContext,
|
||||||
|
const TestIntent(),
|
||||||
|
);
|
||||||
|
expect(enabled1, isTrue);
|
||||||
|
expect(result, isTrue);
|
||||||
|
expect(invoked1, isTrue);
|
||||||
|
|
||||||
|
bool enabledChanged;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: action1,
|
||||||
|
SecondTestIntent: action2,
|
||||||
|
},
|
||||||
|
child: ActionListener(
|
||||||
|
listener: (Action<Intent> action) => enabledChanged = action.enabled,
|
||||||
|
action: action2,
|
||||||
|
child: Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
ThirdTestIntent: action3,
|
||||||
|
},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
result = Actions.invoke<TestIntent>(
|
||||||
|
containerKey.currentContext,
|
||||||
|
const SecondTestIntent(),
|
||||||
|
);
|
||||||
|
expect(enabledChanged, isNull);
|
||||||
|
expect(enabled2, isFalse);
|
||||||
|
expect(result, isFalse);
|
||||||
|
expect(invoked2, isFalse);
|
||||||
|
|
||||||
|
action2.enabled = true;
|
||||||
|
expect(enabledChanged, isTrue);
|
||||||
|
result = Actions.invoke<TestIntent>(
|
||||||
|
containerKey.currentContext,
|
||||||
|
const SecondTestIntent(),
|
||||||
|
);
|
||||||
|
expect(enabled2, isTrue);
|
||||||
|
expect(result, isTrue);
|
||||||
|
expect(invoked2, isTrue);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: action1,
|
||||||
|
},
|
||||||
|
child: Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
ThirdTestIntent: action3,
|
||||||
|
},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(action1.listeners.length, equals(2));
|
||||||
|
expect(action2.listeners.length, equals(1));
|
||||||
|
expect(action3.listeners.length, equals(2));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: action1,
|
||||||
|
ThirdTestIntent: action3,
|
||||||
|
},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(action1.listeners.length, equals(2));
|
||||||
|
expect(action2.listeners.length, equals(1));
|
||||||
|
expect(action3.listeners.length, equals(2));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Actions(
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
TestIntent: action1,
|
||||||
|
},
|
||||||
|
child: Container(key: containerKey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(action1.listeners.length, equals(2));
|
||||||
|
expect(action2.listeners.length, equals(1));
|
||||||
|
expect(action3.listeners.length, equals(1));
|
||||||
|
|
||||||
|
await tester.pumpWidget(Container());
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(action1.listeners.length, equals(1));
|
||||||
|
expect(action2.listeners.length, equals(1));
|
||||||
|
expect(action3.listeners.length, equals(1));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('Diagnostics', () {
|
group('Diagnostics', () {
|
||||||
testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
|
testWidgets('default Intent debugFillProperties', (WidgetTester tester) async {
|
||||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
|
||||||
const Intent(ValueKey<String>('foo')).debugFillProperties(builder);
|
// ignore: invalid_use_of_protected_member
|
||||||
|
const TestIntent().debugFillProperties(builder);
|
||||||
|
|
||||||
final List<String> description = builder.properties
|
final List<String> description = builder.properties
|
||||||
.where((DiagnosticsNode node) {
|
.where((DiagnosticsNode node) {
|
||||||
@ -343,30 +566,13 @@ void main() {
|
|||||||
.map((DiagnosticsNode node) => node.toString())
|
.map((DiagnosticsNode node) => node.toString())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
expect(description, equals(<String>["key: [<'foo'>]"]));
|
expect(description, isEmpty);
|
||||||
});
|
|
||||||
testWidgets('CallbackAction debugFillProperties', (WidgetTester tester) async {
|
|
||||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
|
||||||
|
|
||||||
CallbackAction(
|
|
||||||
const ValueKey<String>('foo'),
|
|
||||||
onInvoke: (FocusNode node, Intent intent) {},
|
|
||||||
).debugFillProperties(builder);
|
|
||||||
|
|
||||||
final List<String> description = builder.properties
|
|
||||||
.where((DiagnosticsNode node) {
|
|
||||||
return !node.isFiltered(DiagnosticLevel.info);
|
|
||||||
})
|
|
||||||
.map((DiagnosticsNode node) => node.toString())
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
expect(description, equals(<String>["intentKey: [<'foo'>]"]));
|
|
||||||
});
|
});
|
||||||
testWidgets('default Actions debugFillProperties', (WidgetTester tester) async {
|
testWidgets('default Actions debugFillProperties', (WidgetTester tester) async {
|
||||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
|
|
||||||
Actions(
|
Actions(
|
||||||
actions: const <LocalKey, ActionFactory>{},
|
actions: const <Type, Action<Intent>>{},
|
||||||
dispatcher: const ActionDispatcher(),
|
dispatcher: const ActionDispatcher(),
|
||||||
child: Container(),
|
child: Container(),
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
@ -378,6 +584,7 @@ void main() {
|
|||||||
.map((DiagnosticsNode node) => node.toString())
|
.map((DiagnosticsNode node) => node.toString())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
expect(description.length, equals(2));
|
||||||
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
|
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
|
||||||
expect(description[1], equals('actions: {}'));
|
expect(description[1], equals('actions: {}'));
|
||||||
});
|
});
|
||||||
@ -387,8 +594,8 @@ void main() {
|
|||||||
Actions(
|
Actions(
|
||||||
key: const ValueKey<String>('foo'),
|
key: const ValueKey<String>('foo'),
|
||||||
dispatcher: const ActionDispatcher(),
|
dispatcher: const ActionDispatcher(),
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
const ValueKey<String>('bar'): () => TestAction(onInvoke: (FocusNode node, Intent intent) {}),
|
TestIntent: TestAction(onInvoke: (Intent intent) => null),
|
||||||
},
|
},
|
||||||
child: Container(key: const ValueKey<String>('baz')),
|
child: Container(key: const ValueKey<String>('baz')),
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
@ -400,8 +607,9 @@ void main() {
|
|||||||
.map((DiagnosticsNode node) => node.toString())
|
.map((DiagnosticsNode node) => node.toString())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
expect(description.length, equals(2));
|
||||||
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
|
expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000'));
|
||||||
expect(description[1], equals("actions: {[<'bar'>]: Closure: () => TestAction}"));
|
expect(description[1], equalsIgnoringHashCodes('actions: {TestIntent: TestAction#00000}'));
|
||||||
}, skip: isBrowser);
|
}, skip: isBrowser);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,15 +8,19 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
class TestAction extends Action {
|
class TestIntent extends Intent {
|
||||||
TestAction() : super(key);
|
const TestIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestAction extends Action<Intent> {
|
||||||
|
TestAction();
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(TestAction);
|
static const LocalKey key = ValueKey<Type>(TestAction);
|
||||||
|
|
||||||
int calls = 0;
|
int calls = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void invoke(FocusNode node, Intent intent) {
|
void invoke(Intent intent) {
|
||||||
calls += 1;
|
calls += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,11 +71,11 @@ void main() {
|
|||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
WidgetsApp(
|
WidgetsApp(
|
||||||
key: key,
|
key: key,
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => action,
|
TestIntent: action,
|
||||||
},
|
},
|
||||||
shortcuts: <LogicalKeySet, Intent> {
|
shortcuts: <LogicalKeySet, Intent> {
|
||||||
LogicalKeySet(LogicalKeyboardKey.space): const Intent(TestAction.key),
|
LogicalKeySet(LogicalKeyboardKey.space): const TestIntent(),
|
||||||
},
|
},
|
||||||
builder: (BuildContext context, Widget child) {
|
builder: (BuildContext context, Widget child) {
|
||||||
return Material(
|
return Material(
|
||||||
|
@ -9,13 +9,13 @@ import 'package:flutter/src/services/keyboard_key.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.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 {
|
class TestAction extends CallbackAction<TestIntent> {
|
||||||
const TestAction({
|
TestAction({
|
||||||
@required OnInvokeCallback onInvoke,
|
@required OnInvokeCallback onInvoke,
|
||||||
}) : assert(onInvoke != null),
|
}) : assert(onInvoke != null),
|
||||||
super(key, onInvoke: onInvoke);
|
super(onInvoke: onInvoke);
|
||||||
|
|
||||||
static const LocalKey key = ValueKey<Type>(TestAction);
|
static const LocalKey key = ValueKey<Type>(TestAction);
|
||||||
}
|
}
|
||||||
@ -26,15 +26,15 @@ class TestDispatcher extends ActionDispatcher {
|
|||||||
final PostInvokeCallback postInvoke;
|
final PostInvokeCallback postInvoke;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool invokeAction(Action action, Intent intent, {FocusNode focusNode}) {
|
Object invokeAction(Action<TestIntent> action, Intent intent, [BuildContext context]) {
|
||||||
final bool result = super.invokeAction(action, intent, focusNode: focusNode);
|
final Object result = super.invokeAction(action, intent, context);
|
||||||
postInvoke?.call(action: action, intent: intent, focusNode: focusNode, dispatcher: this);
|
postInvoke?.call(action: action, intent: intent, context: context, dispatcher: this);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestIntent extends Intent {
|
class TestIntent extends Intent {
|
||||||
const TestIntent() : super(TestAction.key);
|
const TestIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestShortcutManager extends ShortcutManager {
|
class TestShortcutManager extends ShortcutManager {
|
||||||
@ -210,10 +210,11 @@ void main() {
|
|||||||
bool invoked = false;
|
bool invoked = false;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Actions(
|
Actions(
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => TestAction(
|
TestIntent: TestAction(
|
||||||
onInvoke: (FocusNode node, Intent intent) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -247,10 +248,11 @@ void main() {
|
|||||||
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => TestAction(
|
TestIntent: TestAction(
|
||||||
onInvoke: (FocusNode node, Intent intent) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
|
return invoked;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -285,10 +287,11 @@ void main() {
|
|||||||
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
LogicalKeySet(LogicalKeyboardKey.shift): const TestIntent(),
|
||||||
},
|
},
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: <LocalKey, ActionFactory>{
|
actions: <Type, Action<Intent>>{
|
||||||
TestAction.key: () => TestAction(
|
TestIntent: TestAction(
|
||||||
onInvoke: (FocusNode node, Intent intent) {
|
onInvoke: (Intent intent) {
|
||||||
invoked = true;
|
invoked = true;
|
||||||
|
return invoked;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -317,7 +320,7 @@ void main() {
|
|||||||
Shortcuts(shortcuts: <LogicalKeySet, Intent>{LogicalKeySet(
|
Shortcuts(shortcuts: <LogicalKeySet, Intent>{LogicalKeySet(
|
||||||
LogicalKeyboardKey.shift,
|
LogicalKeyboardKey.shift,
|
||||||
LogicalKeyboardKey.keyA,
|
LogicalKeyboardKey.keyA,
|
||||||
) : const Intent(ActivateAction.key),
|
) : const ActivateIntent(),
|
||||||
LogicalKeySet(
|
LogicalKeySet(
|
||||||
LogicalKeyboardKey.shift,
|
LogicalKeyboardKey.shift,
|
||||||
LogicalKeyboardKey.arrowRight,
|
LogicalKeyboardKey.arrowRight,
|
||||||
@ -334,7 +337,7 @@ void main() {
|
|||||||
expect(
|
expect(
|
||||||
description[0],
|
description[0],
|
||||||
equalsIgnoringHashCodes(
|
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.', () {
|
test('Shortcuts diagnostics work when debugLabel specified.', () {
|
||||||
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
@ -345,7 +348,7 @@ void main() {
|
|||||||
LogicalKeySet(
|
LogicalKeySet(
|
||||||
LogicalKeyboardKey.keyA,
|
LogicalKeyboardKey.keyA,
|
||||||
LogicalKeyboardKey.keyB,
|
LogicalKeyboardKey.keyB,
|
||||||
): const Intent(ActivateAction.key)
|
): const ActivateIntent(),
|
||||||
},
|
},
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
|
|
||||||
@ -368,7 +371,7 @@ void main() {
|
|||||||
LogicalKeySet(
|
LogicalKeySet(
|
||||||
LogicalKeyboardKey.keyA,
|
LogicalKeyboardKey.keyA,
|
||||||
LogicalKeyboardKey.keyB,
|
LogicalKeyboardKey.keyB,
|
||||||
): const Intent(ActivateAction.key)
|
): const ActivateIntent(),
|
||||||
},
|
},
|
||||||
).debugFillProperties(builder);
|
).debugFillProperties(builder);
|
||||||
|
|
||||||
@ -381,7 +384,7 @@ void main() {
|
|||||||
|
|
||||||
expect(description.length, equals(2));
|
expect(description.length, equals(2));
|
||||||
expect(description[0], equalsIgnoringHashCodes('manager: ShortcutManager#00000(shortcuts: {})'));
|
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}'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user