Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
590cc27b
Unverified
Commit
590cc27b
authored
Apr 23, 2019
by
Greg Spencer
Committed by
GitHub
Apr 23, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revert "Implement focus traversal for desktop platforms, shoehorn edition. (#30040)" (#31461)
This reverts commit
4218c0bc
.
parent
096439b4
Changes
23
Hide whitespace changes
Inline
Side-by-side
Showing
23 changed files
with
1070 additions
and
2767 deletions
+1070
-2767
raw_keyboard.dart
dev/manual_tests/lib/raw_keyboard.dart
+1
-1
stateful_widget_scaffold.tmpl
dev/snippets/config/templates/stateful_widget_scaffold.tmpl
+1
-1
stateless_widget_scaffold.tmpl
dev/snippets/config/templates/stateless_widget_scaffold.tmpl
+1
-1
keyboard_key.tmpl
dev/tools/gen_keycodes/data/keyboard_key.tmpl
+1
-1
tab_scaffold.dart
packages/flutter/lib/src/cupertino/tab_scaffold.dart
+2
-2
search.dart
packages/flutter/lib/src/material/search.dart
+10
-18
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+5
-10
keyboard_key.dart
packages/flutter/lib/src/services/keyboard_key.dart
+1
-1
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+5
-7
focus_manager.dart
packages/flutter/lib/src/widgets/focus_manager.dart
+330
-909
focus_scope.dart
packages/flutter/lib/src/widgets/focus_scope.dart
+87
-396
framework.dart
packages/flutter/lib/src/widgets/framework.dart
+1
-1
navigator.dart
packages/flutter/lib/src/widgets/navigator.dart
+2
-2
raw_keyboard_listener.dart
packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
+1
-2
routes.dart
packages/flutter/lib/src/widgets/routes.dart
+11
-21
tab_scaffold_test.dart
packages/flutter/test/cupertino/tab_scaffold_test.dart
+2
-8
text_field_focus_test.dart
packages/flutter/test/material/text_field_focus_test.dart
+2
-5
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+17
-25
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+3
-8
focus_manager_test.dart
packages/flutter/test/widgets/focus_manager_test.dart
+0
-523
focus_scope_test.dart
packages/flutter/test/widgets/focus_scope_test.dart
+574
-807
raw_keyboard_listener_test.dart
...ages/flutter/test/widgets/raw_keyboard_listener_test.dart
+13
-17
binding.dart
packages/flutter_test/lib/src/binding.dart
+0
-1
No files found.
dev/manual_tests/lib/raw_keyboard.dart
View file @
590cc27b
...
...
@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
if
(!
_focusNode
.
hasFocus
)
{
return
GestureDetector
(
onTap:
()
{
_focusNode
.
requestFocus
(
);
FocusScope
.
of
(
context
).
requestFocus
(
_focusNode
);
},
child:
Text
(
'Tap to focus'
,
style:
textTheme
.
display1
),
);
...
...
dev/snippets/config/templates/stateful_widget_scaffold.tmpl
View file @
590cc27b
...
...
@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title:
const
Text(_title)),
appBar: AppBar(title: Text(_title)),
body: MyStatefulWidget(),
),
);
...
...
dev/snippets/config/templates/stateless_widget_scaffold.tmpl
View file @
590cc27b
...
...
@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: _title,
home: Scaffold(
appBar: AppBar(title:
const
Text(_title)),
appBar: AppBar(title: Text(_title)),
body: MyStatelessWidget(),
),
);
...
...
dev/tools/gen_keycodes/data/keyboard_key.tmpl
View file @
590cc27b
...
...
@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode);
/// },
/// child:
const
Text('Tap to focus'),
/// child: Text('Tap to focus'),
/// );
/// }
/// return Text(_message ?? 'Press a key');
...
...
packages/flutter/lib/src/cupertino/tab_scaffold.dart
View file @
590cc27b
...
...
@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
tabs
=
List
<
Widget
>(
widget
.
tabNumber
);
tabFocusNodes
=
List
<
FocusScopeNode
>.
generate
(
widget
.
tabNumber
,
(
int
index
)
=>
FocusScopeNode
(
debugLabel:
'Tab Focus Scope
$index
'
),
(
int
index
)
=>
FocusScopeNode
(),
);
}
...
...
@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
@override
void
dispose
()
{
for
(
FocusScopeNode
focusScopeNode
in
tabFocusNodes
)
{
focusScopeNode
.
d
ispose
();
focusScopeNode
.
d
etach
();
}
super
.
dispose
();
}
...
...
packages/flutter/lib/src/material/search.dart
View file @
590cc27b
...
...
@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> {
///
/// * [showSuggestions] to show the search suggestions again.
void
showResults
(
BuildContext
context
)
{
_focusNode
?
.
unfocus
();
_focusNode
.
unfocus
();
_currentBody
=
_SearchBody
.
results
;
}
...
...
@@ -208,8 +208,7 @@ abstract class SearchDelegate<T> {
///
/// * [showResults] to show the search results.
void
showSuggestions
(
BuildContext
context
)
{
assert
(
_focusNode
!=
null
,
'_focusNode must be set by route before showSuggestions is called.'
);
_focusNode
.
requestFocus
();
FocusScope
.
of
(
context
).
requestFocus
(
_focusNode
);
_currentBody
=
_SearchBody
.
suggestions
;
}
...
...
@@ -219,7 +218,7 @@ abstract class SearchDelegate<T> {
/// to [showSearch] that launched the search initially.
void
close
(
BuildContext
context
,
T
result
)
{
_currentBody
=
null
;
_focusNode
?
.
unfocus
();
_focusNode
.
unfocus
();
Navigator
.
of
(
context
)
..
popUntil
((
Route
<
dynamic
>
route
)
=>
route
==
_route
)
..
pop
(
result
);
...
...
@@ -233,9 +232,7 @@ abstract class SearchDelegate<T> {
/// page.
Animation
<
double
>
get
transitionAnimation
=>
_proxyAnimation
;
// The focus node to use for manipulating focus on the search page. This is
// managed, owned, and set by the _SearchPageRoute using this delegate.
FocusNode
_focusNode
;
final
FocusNode
_focusNode
=
FocusNode
();
final
TextEditingController
_queryTextController
=
TextEditingController
();
...
...
@@ -249,6 +246,7 @@ abstract class SearchDelegate<T> {
}
_SearchPageRoute
<
T
>
_route
;
}
/// Describes the body that is currently shown under the [AppBar] in the
...
...
@@ -348,18 +346,13 @@ class _SearchPage<T> extends StatefulWidget {
}
class
_SearchPageState
<
T
>
extends
State
<
_SearchPage
<
T
>>
{
// This node is owned, but not hosted by, the search page. Hosting is done by
// the text field.
FocusNode
focusNode
=
FocusNode
();
@override
void
initState
()
{
super
.
initState
();
queryTextController
.
addListener
(
_onQueryChanged
);
widget
.
animation
.
addStatusListener
(
_onAnimationStatusChanged
);
widget
.
delegate
.
_currentBodyNotifier
.
addListener
(
_onSearchBodyChanged
);
focusNode
.
addListener
(
_onFocusChanged
);
widget
.
delegate
.
_focusNode
=
focusNode
;
widget
.
delegate
.
_focusNode
.
addListener
(
_onFocusChanged
);
}
@override
...
...
@@ -368,8 +361,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
queryTextController
.
removeListener
(
_onQueryChanged
);
widget
.
animation
.
removeStatusListener
(
_onAnimationStatusChanged
);
widget
.
delegate
.
_currentBodyNotifier
.
removeListener
(
_onSearchBodyChanged
);
widget
.
delegate
.
_focusNode
=
null
;
focusNode
.
dispose
();
widget
.
delegate
.
_focusNode
.
removeListener
(
_onFocusChanged
);
}
void
_onAnimationStatusChanged
(
AnimationStatus
status
)
{
...
...
@@ -378,12 +370,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
}
widget
.
animation
.
removeStatusListener
(
_onAnimationStatusChanged
);
if
(
widget
.
delegate
.
_currentBody
==
_SearchBody
.
suggestions
)
{
focusNode
.
requestFocus
(
);
FocusScope
.
of
(
context
).
requestFocus
(
widget
.
delegate
.
_focusNode
);
}
}
void
_onFocusChanged
()
{
if
(
focusNode
.
hasFocus
&&
widget
.
delegate
.
_currentBody
!=
_SearchBody
.
suggestions
)
{
if
(
widget
.
delegate
.
_
focusNode
.
hasFocus
&&
widget
.
delegate
.
_currentBody
!=
_SearchBody
.
suggestions
)
{
widget
.
delegate
.
showSuggestions
(
context
);
}
}
...
...
@@ -444,7 +436,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
leading:
widget
.
delegate
.
buildLeading
(
context
),
title:
TextField
(
controller:
queryTextController
,
focusNode:
focusNode
,
focusNode:
widget
.
delegate
.
_
focusNode
,
style:
theme
.
textTheme
.
title
,
textInputAction:
TextInputAction
.
search
,
onSubmitted:
(
String
_
)
{
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
590cc27b
...
...
@@ -1455,8 +1455,7 @@ class RenderEditable extends RenderBox {
}
TextSelection
_selectWordAtOffset
(
TextPosition
position
)
{
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
,
'Last width (
$_textLayoutLastWidth
) not the same as max width constraint (
${constraints.maxWidth}
).'
);
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
);
final
TextRange
word
=
_textPainter
.
getWordBoundary
(
position
);
// When long-pressing past the end of the text, we want a collapsed cursor.
if
(
position
.
offset
>=
word
.
end
)
...
...
@@ -1528,8 +1527,7 @@ class RenderEditable extends RenderBox {
}
void
_paintCaret
(
Canvas
canvas
,
Offset
effectiveOffset
,
TextPosition
textPosition
)
{
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
,
'Last width (
$_textLayoutLastWidth
) not the same as max width constraint (
${constraints.maxWidth}
).'
);
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
);
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor;
...
...
@@ -1595,8 +1593,7 @@ class RenderEditable extends RenderBox {
}
void
_paintFloatingCaret
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
,
'Last width (
$_textLayoutLastWidth
) not the same as max width constraint (
${constraints.maxWidth}
).'
);
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
);
assert
(
_floatingCursorOn
);
// We always want the floating cursor to render at full opacity.
...
...
@@ -1683,8 +1680,7 @@ class RenderEditable extends RenderBox {
}
void
_paintSelection
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
,
'Last width (
$_textLayoutLastWidth
) not the same as max width constraint (
${constraints.maxWidth}
).'
);
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
);
assert
(
_selectionRects
!=
null
);
final
Paint
paint
=
Paint
()..
color
=
_selectionColor
;
for
(
ui
.
TextBox
box
in
_selectionRects
)
...
...
@@ -1692,8 +1688,7 @@ class RenderEditable extends RenderBox {
}
void
_paintContents
(
PaintingContext
context
,
Offset
offset
)
{
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
,
'Last width (
$_textLayoutLastWidth
) not the same as max width constraint (
${constraints.maxWidth}
).'
);
assert
(
_textLayoutLastWidth
==
constraints
.
maxWidth
);
final
Offset
effectiveOffset
=
offset
+
_paintOffset
;
bool
showSelection
=
false
;
...
...
packages/flutter/lib/src/services/keyboard_key.dart
View file @
590cc27b
...
...
@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode);
/// },
/// child:
const
Text('Tap to focus'),
/// child: Text('Tap to focus'),
/// );
/// }
/// return Text(_message ?? 'Press a key');
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
590cc27b
...
...
@@ -787,7 +787,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final
LayerLink
_layerLink
=
LayerLink
();
bool
_didAutoFocus
=
false
;
FocusAttachment
_focusAttachment
;
// This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out.
...
...
@@ -810,7 +809,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void
initState
()
{
super
.
initState
();
widget
.
controller
.
addListener
(
_didChangeTextEditingValue
);
_focusAttachment
=
widget
.
focusNode
.
attach
(
context
);
widget
.
focusNode
.
addListener
(
_handleFocusChanged
);
_scrollController
.
addListener
(()
{
_selectionOverlay
?.
updateForScroll
();
});
_cursorBlinkOpacityController
=
AnimationController
(
vsync:
this
,
duration:
_fadeDuration
);
...
...
@@ -838,8 +836,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
if
(
widget
.
focusNode
!=
oldWidget
.
focusNode
)
{
oldWidget
.
focusNode
.
removeListener
(
_handleFocusChanged
);
_focusAttachment
?.
detach
();
_focusAttachment
=
widget
.
focusNode
.
attach
(
context
);
widget
.
focusNode
.
addListener
(
_handleFocusChanged
);
updateKeepAlive
();
}
...
...
@@ -856,7 +852,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert
(
_cursorTimer
==
null
);
_selectionOverlay
?.
dispose
();
_selectionOverlay
=
null
;
_focusAttachment
.
detach
();
widget
.
focusNode
.
removeListener
(
_handleFocusChanged
);
super
.
dispose
();
}
...
...
@@ -1096,7 +1091,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if
(
_hasFocus
)
{
_openInputConnection
();
}
else
{
widget
.
focusNode
.
requestFocus
();
final
List
<
FocusScopeNode
>
ancestorScopes
=
FocusScope
.
ancestorsOf
(
context
);
for
(
int
i
=
ancestorScopes
.
length
-
1
;
i
>=
1
;
i
-=
1
)
ancestorScopes
[
i
].
setFirstFocus
(
ancestorScopes
[
i
-
1
]);
FocusScope
.
of
(
context
).
requestFocus
(
widget
.
focusNode
);
}
}
...
...
@@ -1402,7 +1400,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override
Widget
build
(
BuildContext
context
)
{
assert
(
debugCheckHasMediaQuery
(
context
));
_focusAttachment
.
reparent
(
);
FocusScope
.
of
(
context
).
reparentIfNeeded
(
widget
.
focusNode
);
super
.
build
(
context
);
// See AutomaticKeepAliveClientMixin.
final
TextSelectionControls
controls
=
widget
.
selectionControls
;
...
...
packages/flutter/lib/src/widgets/focus_manager.dart
View file @
590cc27b
...
...
@@ -3,505 +3,61 @@
// found in the LICENSE file.
import
'dart:async'
;
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/painting.dart'
;
import
'package:flutter/services.dart'
;
import
'binding.dart'
;
import
'focus_scope.dart'
;
import
'framework.dart'
;
/// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey]
/// to receive key events.
///
/// The [node] is the node that received the event.
typedef
FocusOnKeyCallback
=
bool
Function
(
FocusNode
node
,
RawKeyEvent
event
);
/// An attachment point for a [FocusNode].
///
/// Once created, a [FocusNode] must be attached to the widget tree by its
/// _host_ [StatefulWidget] via a [FocusAttachment] object. [FocusAttachment]s
/// are owned by the [StatefulWidget] that hosts a [FocusNode] or
/// [FocusScopeNode]. There can be multiple [FocusAttachment]s for each
/// [FocusNode], but the node will only ever be attached to one of them at a
/// time.
///
/// This attachment is created by calling [FocusNode.attach], usually from the
/// host widget's [State.initState] method. If the widget is updated to have a
/// different focus node, then the new node needs to be attached in
/// [State.didUpdateWidget], after calling [detach] on the previous
/// [FocusAttachment]. Once detached, the attachment is defunct and will no
/// longer make changes to the [FocusNode] through [reparent].
///
/// Without these attachment points, it would be possible for a focus node to
/// simultaneously be attached to more than one part of the widget tree during
/// the build stage.
class
FocusAttachment
{
/// A private constructor, because [FocusAttachment]s are only to be created
/// by [FocusNode.attach].
FocusAttachment
.
_
(
this
.
_node
)
:
assert
(
_node
!=
null
);
// The focus node that this attachment manages an attachment for. The node may
// not yet have a parent, or may have been detached from this attachment, so
// don't count on this node being in a usable state.
final
FocusNode
_node
;
/// Returns true if the associated node is attached to this attachment.
///
/// It is possible to be attached to the widget tree, but not be placed in
/// the focus tree (i.e. to not have a parent yet in the focus tree).
bool
get
isAttached
=>
_node
.
_attachment
==
this
;
/// Detaches the [FocusNode] this attachment point is associated with from the
/// focus tree, and disconnects it from this attachment point.
///
/// Calling [FocusNode.dispose] will also automatically detach the node.
void
detach
()
{
assert
(
_node
!=
null
);
if
(
isAttached
)
{
_node
.
_parent
?.
_removeChild
(
_node
);
_node
.
_attachment
=
null
;
}
assert
(!
isAttached
);
}
/// Ensures that the [FocusNode] attached at this attachment point has the
/// proper parent node, changing it if necessary.
///
/// If given, ensures that the given [parent] node is the parent of the node
/// that is attached at this attachment point, changing it if necessary.
/// However, it is usually not necessary to supply an explicit parent, since
/// [reparent] will use [Focus.of] to determine the correct parent node for
/// the context given in [FocusNode.attach].
///
/// If [isAttached] is false, then calling this method does nothing.
///
/// Should be called whenever the associated widget is rebuilt in order to
/// maintain the focus hierarchy.
///
/// A [StatefulWidget] that hosts a [FocusNode] should call this method on the
/// node it hosts during its [State.build] or [State.didChangeDependencies]
/// methods in case the widget is moved from one location in the tree to
/// another location that has a different [FocusScope] or context.
///
/// The optional [parent] argument must be supplied when not using [Focus] and
/// [FocusScope] widgets to build the focus tree, or if there is a need to
/// supply the parent explicitly (which are both uncommon).
void
reparent
({
FocusNode
parent
})
{
assert
(
_node
!=
null
);
if
(
isAttached
)
{
assert
(
_node
.
context
!=
null
);
parent
??=
Focus
.
of
(
_node
.
context
);
assert
(
parent
!=
null
);
parent
.
_reparent
(
_node
);
}
}
}
/// An object that can be used by a stateful widget to obtain the keyboard focus
/// and to handle keyboard events.
///
/// _Please see the [Focus] and [FocusScope] widgets, which are utility widgets
/// that manage their own [FocusNode]s and [FocusScopeNode]s, respectively. If
/// they aren't appropriate, [FocusNode]s can be managed directly._
///
/// [FocusNode]s are persistent objects that form a _focus tree_ that is a
/// representation of the widgets in the hierarchy that are interested in focus.
/// A focus node might need to be created if it is passed in from an ancestor of
/// a [Focus] widget to control the focus of the children from the ancestor, or
/// a widget might need to host one if the widget subsystem is not being used,
/// or if the [Focus] and [FocusScope] widgets provide insufficient control.
///
/// [FocusNodes] are organized into _scopes_ (see [FocusScopeNode]), which form
/// sub-trees of nodes that can be traversed as a group. Within a scope, the
/// most recent nodes to have focus are remembered, and if a node is focused and
/// then removed, the previous node receives focus again.
///
/// The focus node hierarchy can be traversed using the [parent], [children],
/// [ancestors] and [descendants] accessors.
///
/// [FocusNode]s are [ChangeNotifier]s, so a listener can be registered to
/// receive a notification when the focus changes. If the [Focus] and
/// [FocusScope] widgets are being used to manage the nodes, consider
/// establishing an [InheritedWidget] dependency on them by calling [Focus.of]
/// or [FocusScope.of] instead. [Focus.hasFocus] can also be used to establish a
/// similar dependency, especially if all that is needed is to determine whether
/// or not the widget is focused at build time.
///
/// To see the focus tree in the debug console, call [debugDumpFocusTree]. To
/// get the focus tree as a string, call [debugDescribeFocusTree].
///
/// {@template flutter.widgets.focus_manager.focus.lifecycle}
/// ## Lifecycle
///
/// There are several actors involved in the lifecycle of a
/// [FocusNode]/[FocusScopeNode]. They are created and disposed by their
/// _owner_, attached, detached, and reparented using a [FocusAttachment] by
/// their _host_ (which must be owned by the [State] of a [StatefulWidget]), and
/// they are managed by the [FocusManager]. Different parts of the [FocusNode]
/// API are intended for these different actors.
///
/// [FocusNode]s (and hence [FocusScopeNode]s) are persistent objects that form
/// part of a _focus tree_ that is a sparse representation of the widgets in the
/// hierarchy that are interested in receiving keyboard events. They must be
/// managed like other persistent state, which is typically done by a
/// [StatefulWidget] that owns the node. A stateful widget that owns a focus
/// scope node must call [dispose] from its [State.dispose] method.
///
/// Once created, a [FocusNode] must be attached to the widget tree via a
/// [FocusAttachment] object. This attachment is created by calling [attach],
/// usually from the [State.initState] method. If the hosting widget is updated
/// to have a different focus node, then the updated node needs to be attached
/// in [State.didUpdateWidget], after calling [detach] on the previous
/// [FocusAttachment].
///
/// Because [FocusNode]s form a sparse representation of the widget tree,
/// they must be updated whenever the widget tree is rebuilt. This is done by
/// calling [FocusAttachment.reparent], usually from the [State.build] or
/// [State.didChangeDependencies] methods of the widget that represents the
/// focused region, so that the [BuildContext] assigned to the [FocusScopeNode]
/// can be tracked (the context is used to obtain the [RenderObject], from which
/// the geometry of focused regions can be determined).
/// A leaf node in the focus tree that can receive focus.
///
/// Creating a [FocusNode] each time [State.build] is invoked will cause the
/// focus to be lost each time the widget is built, which is usually not desired
/// behavior (call [unfocus] if losing focus is desired).
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// If, as is common, the hosting [StatefulWidget] is also the owner of the
/// focus node, then it will also call [dispose] from its [State.dispose] (in
/// which case the [detach] may be skipped, since dispose will automatically
/// detach). If another object owns the focus node, then it must call [dispose]
/// when the node is done being used.
/// {@endtemplate}
/// To request focus, find the [FocusScopeNode] for the current [BuildContext]
/// and call the [FocusScopeNode.requestFocus] method:
///
/// {@template flutter.widgets.focus_manager.focus.keyEvents}
/// ## Key Event Propagation
///
/// The [FocusManager] receives all key events and will pass them to the focused
/// nodes. It starts with the node with the primary focus, and will call the
/// [onKey] callback for that node. If the callback returns false, indicating
/// that it did not handle the event, the [FocusManager] will move to the parent
/// of that node and call its [onKey]. If that [onKey] returns true, then it
/// will stop propagating the event. If it reaches the root [FocusScopeNode],
/// [FocusManager.rootScope], the event is discarded.
/// {@endtemplate}
///
/// {@tool snippet --template=stateless_widget_scaffold}
/// This example shows how a FocusNode should be managed if not using the
/// [Focus] or [FocusScope] widgets. See the [Focus] widget for a similar
/// example using [Focus] and [FocusScope] widgets.
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```dart
/// FocusScope.of(context).requestFocus(focusNode);
/// ```
///
/// ```dart preamble
/// class ColorfulButton extends StatefulWidget {
/// ColorfulButton({Key key}) : super(key: key);
///
/// @override
/// _ColorfulButtonState createState() => _ColorfulButtonState();
/// }
///
/// class _ColorfulButtonState extends State<ColorfulButton> {
/// FocusNode _node;
/// FocusAttachment _nodeAttachment;
/// Color _color = Colors.white;
///
/// @override
/// void initState() {
/// super.initState();
/// _node = FocusNode(debugLabel: 'Button');
/// _nodeAttachment = _node.attach(context, onKey: _handleKeyPress);
/// }
///
/// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// if (event is RawKeyDownEvent) {
/// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
/// if (event.logicalKey == LogicalKeyboardKey.keyR) {
/// print('Changing color to red.');
/// setState(() {
/// _color = Colors.red;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyG) {
/// print('Changing color to green.');
/// setState(() {
/// _color = Colors.green;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyB) {
/// print('Changing color to blue.');
/// setState(() {
/// _color = Colors.blue;
/// });
/// return true;
/// }
/// }
/// return false;
/// }
///
/// @override
/// void dispose() {
/// // The attachment will automatically be detached in dispose().
/// _node.dispose();
/// super.dispose();
/// }
/// If your widget requests focus, be sure to call
/// `FocusScope.of(context).reparentIfNeeded(focusNode);` in your `build`
/// method to reparent your [FocusNode] if your widget moves from one
/// location in the tree to another.
///
/// @override
/// Widget build(BuildContext context) {
/// _nodeAttachment.reparent();
/// return GestureDetector(
/// onTap: () {
/// if (_node.hasFocus) {
/// setState(() {
/// _node.unfocus();
/// });
/// } else {
/// setState(() {
/// _node.requestFocus();
/// });
/// }
/// },
/// child: Center(
/// child: Container(
/// width: 400,
/// height: 100,
/// color: _node.hasFocus ? _color : Colors.white,
/// alignment: Alignment.center,
/// child: Text(
/// _node.hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
/// ## Lifetime
///
/// ```dart
/// Widget build(BuildContext context) {
/// final TextTheme textTheme = Theme.of(context).textTheme;
/// return DefaultTextStyle(
/// style: textTheme.display1,
/// child: ColorfulButton(),
/// );
/// }
/// ```
/// {@end-tool}
/// Focus nodes are long-lived objects. For example, if a stateful widget has a
/// focusable child widget, it should create a [FocusNode] in the
/// [State.initState] method, and [dispose] it in the [State.dispose] method,
/// providing the same [FocusNode] to the focusable child each time the
/// [State.build] method is run. In particular, creating a [FocusNode] each time
/// [State.build] is invoked will cause the focus to be lost each time the
/// widget is built.
///
/// See also:
///
/// * [Focus], a widget that manages a [FocusNode] and provides access to
/// focus information and actions to its descendant widgets.
/// * [FocusScope], a widget that manages a [FocusScopeNode] and provides
/// access to scope information and actions to its descendant widgets.
/// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the
/// widget tree.
/// * [FocusManager], a singleton that manages the focus and distributes key
/// events to focused nodes.
class
FocusNode
with
DiagnosticableTreeMixin
,
ChangeNotifier
{
/// Creates a focus node.
///
/// The [debugLabel] is ignored on release builds.
FocusNode
({
String
debugLabel
,
FocusOnKeyCallback
onKey
,
})
:
_onKey
=
onKey
{
// Set it via the setter so that it does nothing on release builds.
this
.
debugLabel
=
debugLabel
;
}
/// The context that was supplied to [attach].
///
/// This is typically the context for the widget that is being focused, as it
/// is used to determine the bounds of the widget.
BuildContext
get
context
=>
_context
;
BuildContext
_context
;
/// Called if this focus node receives a key event while focused (i.e. when
/// [hasFocus] returns true).
///
/// {@macro flutter.widgets.focus_manager.focus.keyEvents}
FocusOnKeyCallback
get
onKey
=>
_onKey
;
FocusOnKeyCallback
_onKey
;
/// * [FocusScopeNode], which is an interior node in the focus tree.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext].
class
FocusNode
extends
ChangeNotifier
{
FocusScopeNode
_parent
;
FocusManager
_manager
;
bool
_hasKeyboardToken
=
false
;
///
Returns the parent node for this object
.
///
Whether this node has the overall focus
.
///
/// All nodes except for the root [FocusScopeNode] ([FocusManager.rootScope])
/// will be given a parent when they are added to the focus tree, which is
/// done using [FocusAttachment.reparent].
FocusNode
get
parent
=>
_parent
;
FocusNode
_parent
;
/// An iterator over the children of this node.
Iterable
<
FocusNode
>
get
children
=>
_children
;
final
List
<
FocusNode
>
_children
=
<
FocusNode
>[];
/// A debug label that is used for diagnostic output.
/// A [FocusNode] has the overall focus when the node is focused in its
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
/// that scope and all its ancestor scopes.
///
/// Will always return null in release builds.
String
get
debugLabel
=>
_debugLabel
;
String
_debugLabel
;
set
debugLabel
(
String
value
)
{
assert
(()
{
// Only set the value in debug builds.
_debugLabel
=
value
;
return
true
;
}());
}
FocusAttachment
_attachment
;
/// An [Iterable] over the hierarchy of children below this one, in
/// depth-first order.
Iterable
<
FocusNode
>
get
descendants
sync
*
{
for
(
FocusNode
child
in
_children
)
{
yield
*
child
.
descendants
;
yield
child
;
}
}
/// An [Iterable] over the ancestors of this node.
///
/// Iterates the ancestors of this node starting at the parent and iterating
/// over successively more remote ancestors of this node, ending at the root
/// [FocusScope] ([FocusManager.rootScope]).
Iterable
<
FocusNode
>
get
ancestors
sync
*
{
FocusNode
parent
=
_parent
;
while
(
parent
!=
null
)
{
yield
parent
;
parent
=
parent
.
_parent
;
}
}
/// Whether this node has input focus.
/// To request focus, find the [FocusScopeNode] for the current [BuildContext]
/// and call the [FocusScopeNode.requestFocus] method:
///
/// A [FocusNode] has focus when it is an ancestor of a node that returns true
/// from [hasPrimaryFocus], or it has the primary focus itself.
///
/// The [hasFocus] accessor is different from [hasPrimaryFocus] in that
/// [hasFocus] is true if the node is anywhere in the focus chain, but for
/// [hasPrimaryFocus] the node must to be at the end of the chain to return
/// true.
///
/// A node that returns true for [hasFocus] will receive key events if none of
/// its focused descendants returned true from their [onKey] handler.
///
/// This object is a [ChangeNotifier], and notifies its [Listenable] listeners
/// (registered via [addListener]) whenever this value changes.
///
/// See also:
///
/// * [Focus.isAt], which is a static method that will return the focus
/// state of the nearest ancestor [Focus] widget's focus node.
bool
get
hasFocus
{
if
(
_manager
?.
_currentFocus
==
null
)
{
return
false
;
}
if
(
hasPrimaryFocus
)
{
return
true
;
}
return
_manager
.
_currentFocus
.
ancestors
.
contains
(
this
);
}
/// Returns true if this node currently has the application-wide input focus.
///
/// A [FocusNode] has the primary focus when the node is focused in its
/// nearest ancestor [FocusScopeNode] and [hasFocus] is true for all its
/// ancestor nodes, but none of its descendants.
///
/// This is different from [hasFocus] in that [hasFocus] is true if the node
/// is anywhere in the focus chain, but here the node has to be at the end of
/// the chain to return true.
///
/// A node that returns true for [hasPrimaryFocus] will be the first node to
/// receive key events through its [onKey] handler.
/// ```dart
/// FocusScope.of(context).requestFocus(focusNode);
/// ```
///
/// This object notifies its listeners whenever this value changes.
bool
get
hasPrimaryFocus
=>
_manager
?.
_currentFocus
==
this
;
/// Returns the nearest enclosing scope node above this node, including
/// this node, if it's a scope.
///
/// Returns null if no scope is found.
///
/// Use [enclosingScope] to look for scopes above this node.
FocusScopeNode
get
nearestScope
=>
enclosingScope
;
/// Returns the nearest enclosing scope node above this node, or null if the
/// node has not yet be added to the focus tree.
///
/// If this node is itself a scope, this will only return ancestors of this
/// scope.
///
/// Use [nearestScope] to start at this node instead of above it.
FocusScopeNode
get
enclosingScope
{
return
ancestors
.
firstWhere
((
FocusNode
node
)
=>
node
is
FocusScopeNode
,
orElse:
()
=>
null
);
}
/// Returns the size of the attached widget's [RenderObject], in logical
/// units.
Size
get
size
{
assert
(
context
!=
null
,
"Tried to get the size of a focus node that didn't have its context set yet.
\n
"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.'
);
return
context
.
findRenderObject
().
semanticBounds
.
size
;
}
/// Returns the global offset to the upper left corner of the attached
/// widget's [RenderObject], in logical units.
Offset
get
offset
{
assert
(
context
!=
null
,
"Tried to get the offset of a focus node that didn't have its context set yet.
\n
"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.'
);
final
RenderObject
object
=
context
.
findRenderObject
();
return
MatrixUtils
.
transformPoint
(
object
.
getTransformTo
(
null
),
object
.
semanticBounds
.
topLeft
);
}
/// Returns the global rectangle of the attached widget's [RenderObject], in
/// logical units.
Rect
get
rect
{
assert
(
context
!=
null
,
"Tried to get the bounds of a focus node that didn't have its context set yet.
\n
"
'The context needs to be set before trying to evaluate traversal policies. This '
'is typically done with the attach method.'
);
final
RenderObject
object
=
context
.
findRenderObject
();
final
Offset
globalOffset
=
MatrixUtils
.
transformPoint
(
object
.
getTransformTo
(
null
),
object
.
semanticBounds
.
topLeft
);
return
globalOffset
&
object
.
semanticBounds
.
size
;
}
/// Removes focus from a node that has the primary focus, and cancels any
/// outstanding requests to focus it.
///
/// Calling [requestFocus] sends a request to the [FocusManager] to make that
/// node the primary focus, which schedules a microtask to resolve the latest
/// request into an update of the focus state on the tree. Calling [unfocus]
/// cancels a request that has been requested, but not yet acted upon.
///
/// This method is safe to call regardless of whether this node has ever
/// requested focus.
///
/// Has no effect on nodes that return true from [hasFocus], but false from
/// [hasPrimaryFocus].
void
unfocus
()
{
final
FocusScopeNode
scope
=
enclosingScope
;
if
(
scope
==
null
)
{
// This node isn't part of a tree.
return
;
}
scope
.
_focusedChildren
.
remove
(
this
);
_manager
?.
_willUnfocusNode
(
this
);
}
bool
get
hasFocus
=>
_manager
?.
_currentFocus
==
this
;
/// Removes the keyboard token from this focus node if it has one.
///
...
...
@@ -512,360 +68,330 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus
/// node receives a keyboard token if it does not already have one. Later,
/// when the focus node becomes focused, the widget that manages the
/// [TextInputConnection] should show the keyboard (i.e. call
/// [TextInputConnection] should show the keyboard (i.e.
,
call
/// [TextInputConnection.show]) only if it successfully consumes the keyboard
/// token from the focus node.
///
/// Returns
true if this method successfully consumes the
keyboard token.
/// Returns
whether this function successfully consumes a
keyboard token.
bool
consumeKeyboardToken
()
{
if
(!
_hasKeyboardToken
)
{
if
(!
_hasKeyboardToken
)
return
false
;
}
_hasKeyboardToken
=
false
;
return
true
;
}
// Marks the node as dirty, meaning that it needs to notify listeners of a
// focus change the next time focus is resolved by the manager.
void
_markAsDirty
({
FocusNode
newFocus
})
{
if
(
_manager
!=
null
)
{
// If we have a manager, then let it handle the focus change.
_manager
.
_dirtyNodes
?.
add
(
this
);
_manager
.
_markNeedsUpdate
(
newFocus:
newFocus
);
}
else
{
// If we don't have a manager, then change the focus locally.
newFocus
?.
_setAsFocusedChild
();
newFocus
?.
_notify
();
if
(
newFocus
!=
this
)
{
_notify
();
}
}
}
// Removes the given FocusNode and its children as a child of this node.
@mustCallSuper
void
_removeChild
(
FocusNode
node
)
{
assert
(
_children
.
contains
(
node
),
"Tried to remove a node that wasn't a child."
);
assert
(
node
.
_parent
==
this
);
assert
(
node
.
_manager
==
_manager
);
// If the child was (or requested to be) the primary focus, then unfocus it
// and cancel any outstanding request to be focused.
node
.
unfocus
();
node
.
_parent
=
null
;
_children
.
remove
(
node
);
assert
(
_manager
==
null
||
!
_manager
.
rootScope
.
descendants
.
contains
(
node
));
}
void
_updateManager
(
FocusManager
manager
)
{
_manager
=
manager
;
for
(
FocusNode
descendant
in
descendants
)
{
descendant
.
_manager
=
manager
;
}
}
// Used by FocusAttachment.reparent to perform the actual parenting operation.
@mustCallSuper
void
_reparent
(
FocusNode
child
)
{
assert
(
child
!=
null
);
assert
(
child
!=
this
,
'Tried to make a child into a parent of itself.'
);
if
(
child
.
_parent
==
this
)
{
assert
(
_children
.
contains
(
child
),
"Found a node that says it's a child, but doesn't appear in the child list."
);
// The child is already a child of this parent.
return
;
}
assert
(
_manager
==
null
||
child
!=
_manager
.
rootScope
,
"Reparenting the root node isn't allowed."
);
assert
(!
ancestors
.
contains
(
child
),
'The supplied child is already an ancestor of this node. Loops are not allowed.'
);
FocusNode
oldPrimaryFocus
;
if
(
child
.
_manager
!=
null
)
{
// We want to find out what the primary focus is, since the new child
// might be an ancestor of the primary focus, and the primary focus should
// move with the child.
oldPrimaryFocus
=
child
.
hasFocus
?
child
.
_manager
.
_currentFocus
:
null
;
assert
(
oldPrimaryFocus
==
null
||
oldPrimaryFocus
==
child
||
oldPrimaryFocus
.
ancestors
.
contains
(
child
),
"child has focus, but primary focus isn't a descendant of it."
);
}
// If the child currently has focus, we have to do some extra work to keep
// that focus, and to notify any scopes that used to be ancestors, and no
// longer have focus after we move it.
final
Set
<
FocusNode
>
oldFocusPath
=
oldPrimaryFocus
?.
ancestors
?.
toSet
()
??
<
FocusNode
>{};
child
.
_parent
?.
_removeChild
(
child
);
_children
.
add
(
child
);
child
.
_parent
=
this
;
child
.
_updateManager
(
_manager
);
if
(
oldPrimaryFocus
!=
null
)
{
final
Set
<
FocusNode
>
newFocusPath
=
_manager
?.
_currentFocus
?.
ancestors
?.
toSet
()
??
<
FocusNode
>{};
// Nodes that will no longer be focused need to be marked dirty.
for
(
FocusNode
node
in
oldFocusPath
.
difference
(
newFocusPath
))
{
node
.
_markAsDirty
();
}
// If the node used to have focus, make sure it keeps it's old primary
// focus when it moves.
oldPrimaryFocus
.
requestFocus
();
}
}
/// Called by the _host_ [StatefulWidget] to attach a [FocusNode] to the
/// widget tree.
///
/// In order to attach a [FocusNode] to the widget tree, call [attach],
/// typically from the [StatefulWidget]'s [State.initState] method.
/// Cancels any outstanding requests for focus.
///
/// If the focus node in the host widget is swapped out, the new node will
/// need to be attached. [FocusAttachment.detach] should be called on the old
/// node, and then [attach] called on the new node. This typically happens in
/// the [State.didUpdateWidget] method.
@mustCallSuper
FocusAttachment
attach
(
BuildContext
context
,
{
FocusOnKeyCallback
onKey
})
{
_context
=
context
;
_onKey
=
onKey
;
_attachment
=
FocusAttachment
.
_
(
this
);
return
_attachment
;
/// This method is safe to call regardless of whether this node has ever
/// requested focus.
void
unfocus
()
{
_parent
?.
_resignFocus
(
this
);
assert
(
_parent
==
null
);
assert
(
_manager
==
null
);
}
@override
void
dispose
()
{
_manager
?.
_willDisposeFocusNode
(
this
);
_attachment
?.
detach
();
_parent
?.
_resignFocus
(
this
);
assert
(
_parent
==
null
);
assert
(
_manager
==
null
);
super
.
dispose
();
}
@mustCallSuper
void
_notify
()
{
if
(
_parent
==
null
)
{
// no longer part of the tree, so don't notify.
return
;
}
if
(
hasPrimaryFocus
)
{
_setAsFocusedChild
();
}
notifyListeners
();
}
/// Requests the primary focus for this node, or for a supplied [node], which
/// will also give focus to its [ancestors].
///
/// If called without a node, request focus for this node.
///
/// If the given [node] is not yet a part of the focus tree, then this method
/// will add the [node] as a child of this node before requesting focus.
///
/// If the given [node] is a [FocusScopeNode] and that focus scope node has a
/// non-null [focusedChild], then request the focus for the focused child.
/// This process is recursive and continues until it encounters either a focus
/// scope node with a null focused child or an ordinary (non-scope)
/// [FocusNode] is found.
///
/// The node is notified that it has received the primary focus in a
/// microtask, so notification may lag the request by up to one frame.
void
requestFocus
([
FocusNode
node
])
{
if
(
node
!=
null
)
{
if
(
node
.
_parent
==
null
)
{
_reparent
(
node
);
}
assert
(
node
.
ancestors
.
contains
(
this
),
'Focus was requested for a node that is not a descendant of the scope from which it was requested.'
);
node
.
_doRequestFocus
(
isFromPolicy:
false
);
return
;
}
_doRequestFocus
(
isFromPolicy:
false
);
}
// Note that this is overridden in FocusScopeNode.
void
_doRequestFocus
({
@required
bool
isFromPolicy
})
{
assert
(
isFromPolicy
!=
null
);
_setAsFocusedChild
();
if
(
hasPrimaryFocus
)
{
return
;
}
_hasKeyboardToken
=
true
;
_markAsDirty
(
newFocus:
this
);
}
// Sets this node as the focused child for the enclosing scope, and that scope
// as the focused child for the scope above it, etc., until it reaches the
// root node. It doesn't change the primary focus, it just changes what node
// would be focused if the enclosing scope receives focus, and keeps track of
// previously focused children so that if one is removed, the previous focus
// returns.
void
_setAsFocusedChild
()
{
FocusNode
scopeFocus
=
this
;
for
(
FocusScopeNode
ancestor
in
ancestors
.
whereType
<
FocusScopeNode
>())
{
assert
(
scopeFocus
!=
ancestor
,
'Somehow made a loop by setting focusedChild to its scope.'
);
// Remove it anywhere in the focused child history.
ancestor
.
_focusedChildren
.
remove
(
scopeFocus
);
// Add it to the end of the list, which is also the top of the queue: The
// end of the list represents the currently focused child.
ancestor
.
_focusedChildren
.
add
(
scopeFocus
);
scopeFocus
=
ancestor
;
}
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
BuildContext
>(
'context'
,
context
,
defaultValue:
null
));
properties
.
add
(
FlagProperty
(
'hasFocus'
,
value:
hasFocus
,
ifTrue:
'FOCUSED'
,
defaultValue:
false
));
properties
.
add
(
StringProperty
(
'debugLabel'
,
debugLabel
,
defaultValue:
null
));
}
@override
List
<
DiagnosticsNode
>
debugDescribeChildren
()
{
int
count
=
1
;
return
_children
.
map
<
DiagnosticsNode
>((
FocusNode
child
)
{
return
child
.
toDiagnosticsNode
(
name:
'Child
${count++}
'
);
}).
toList
();
}
String
toString
()
=>
'
${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}
'
;
}
/// A subclass of [FocusNode] that acts as a scope for its descendants,
/// maintaining information about which descendant is currently or was last
/// focused.
/// An interior node in the focus tree.
///
/// _Please see the [FocusScope] and [Focus] widgets, which are utility widgets
/// that manage their own [FocusScopeNode]s and [FocusNode]s, respectively. If
/// they aren't appropriate, [FocusScopeNode]s can be managed directly._
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
///
[FocusScopeNode] organizes [FocusNodes] into _scopes_. Scopes form sub-trees
///
of nodes that can be traversed as a group. Within a scope, the most recent
///
nodes to have focus are remembered, and if a node is focused and then
///
removed, the original node receives focus again
.
///
The interior nodes in the focus tree cannot themselves be focused but
///
instead remember previous focus states. A scope is currently active in its
///
parent whenever [isFirstFocus] is true. If that scope is detached from its
///
parent, its previous sibling becomes the parent's first focus
.
///
///
From a [FocusScopeNode], calling [setFirstFocus], sets the given focus scope
///
as the [focusedChild] of this node, adopting if it isn't already part of the
///
focus tree
.
///
A [FocusNode] has the overall focus when the node is focused in its
///
parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for
///
that scope and all its ancestor scopes
.
///
///
{@macro flutter.widgets.focusManager.lifecycle}
///
{@macro flutter.widgets.focus_manager.focus.keyEvents}
///
If a [FocusScopeNode] is removed, then the next sibling node will be set as
///
the focused node by the [FocusManager].
///
/// See also:
///
/// * [Focus], a widget that manages a [FocusNode] and provides access to
/// focus information and actions to its descendant widgets.
/// * [FocusScope], a widget that manages a [FocusScopeNode] and provides
/// access to scope information and actions to its descendant widgets.
/// * [FocusAttachment], a widget that connects a [FocusScopeNode] to the
/// focus tree.
/// * [FocusManager], a singleton that manages the focus and distributes key
/// events to focused nodes.
class
FocusScopeNode
extends
FocusNode
{
/// Creates a FocusScope node.
///
/// All parameters are optional.
FocusScopeNode
({
String
debugLabel
,
FocusOnKeyCallback
onKey
,
})
:
super
(
debugLabel:
debugLabel
,
onKey:
onKey
);
/// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given
/// [BuildContext].
/// * [FocusScope], which is a widget that associates a [FocusScopeNode] with
/// its location in the tree.
class
FocusScopeNode
extends
Object
with
DiagnosticableTreeMixin
{
FocusManager
_manager
;
FocusScopeNode
_parent
;
@override
FocusScopeNode
get
nearestScope
=>
this
;
FocusScopeNode
_nextSibling
;
FocusScopeNode
_previousSibling
;
/// Returns true if this scope is the focused child of its parent scope.
bool
get
isFirstFocus
=>
enclosingScope
.
focusedChild
==
this
;
FocusScopeNode
_firstChild
;
FocusScopeNode
_lastChild
;
/// Returns the child of this node that should receive focus if this scope
/// node receives focus.
///
/// If [hasFocus] is true, then this points to the child of this node that is
/// currently focused.
///
/// Returns null if there is no currently focused child.
FocusNode
get
focusedChild
{
assert
(
_focusedChildren
.
isEmpty
||
_focusedChildren
.
last
.
enclosingScope
==
this
,
'Focused child does not have the same idea of its enclosing scope as the scope does.'
);
return
_focusedChildren
.
isNotEmpty
?
_focusedChildren
.
last
:
null
;
FocusNode
_focus
;
List
<
FocusScopeNode
>
_focusPath
;
/// Whether this scope is currently active in its parent scope.
bool
get
isFirstFocus
=>
_parent
==
null
||
_parent
.
_firstChild
==
this
;
// Returns this FocusScopeNode's ancestors, starting with the node
// below the FocusManager's rootScope.
List
<
FocusScopeNode
>
_getFocusPath
()
{
final
List
<
FocusScopeNode
>
nodes
=
<
FocusScopeNode
>[
this
];
FocusScopeNode
node
=
_parent
;
while
(
node
!=
null
&&
node
!=
_manager
?.
rootScope
)
{
nodes
.
add
(
node
);
node
=
node
.
_parent
;
}
return
nodes
;
}
// A stack of the children that have been set as the focusedChild, most recent
// last (which is the top of the stack).
final
List
<
FocusNode
>
_focusedChildren
=
<
FocusNode
>[];
void
_prepend
(
FocusScopeNode
child
)
{
assert
(
child
!=
this
);
assert
(
child
!=
_firstChild
);
assert
(
child
!=
_lastChild
);
assert
(
child
.
_parent
==
null
);
assert
(
child
.
_manager
==
null
);
assert
(
child
.
_nextSibling
==
null
);
assert
(
child
.
_previousSibling
==
null
);
assert
(()
{
FocusScopeNode
node
=
this
;
while
(
node
.
_parent
!=
null
)
node
=
node
.
_parent
;
assert
(
node
!=
child
);
// indicates we are about to create a cycle
return
true
;
}());
child
.
_parent
=
this
;
child
.
_nextSibling
=
_firstChild
;
if
(
_firstChild
!=
null
)
_firstChild
.
_previousSibling
=
child
;
_firstChild
=
child
;
_lastChild
??=
child
;
child
.
_updateManager
(
_manager
);
}
@override
void
_reparent
(
FocusNode
child
)
{
final
bool
hadChildren
=
_children
.
isNotEmpty
;
super
.
_reparent
(
child
);
final
FocusScopeNode
currentEnclosingScope
=
child
.
enclosingScope
;
// If we just added our first child to this scope, and this scope had the
// focus, then focus the child.
if
(!
hadChildren
&&
currentEnclosingScope
.
focusedChild
==
null
&&
currentEnclosingScope
.
hasFocus
)
{
child
.
requestFocus
();
void
_updateManager
(
FocusManager
manager
)
{
void
update
(
FocusScopeNode
child
)
{
if
(
child
.
_manager
==
manager
)
return
;
child
.
_manager
=
manager
;
// We don't proactively null out the manager for FocusNodes because the
// manager holds the currently active focus node until the end of the
// microtask, even if that node is detached from the focus tree.
if
(
manager
!=
null
)
child
.
_focus
?.
_manager
=
manager
;
child
.
_visitChildren
(
update
);
}
update
(
this
);
}
/// Make the given [scope] the active child scope for this scope.
///
/// If the given [scope] is not yet a part of the focus tree, then add it to
/// the tree as a child of this scope.
///
/// The given scope must be a descendant of this scope.
void
setFirstFocus
(
FocusScopeNode
scope
)
{
assert
(
scope
!=
null
);
if
(
scope
.
_parent
==
null
)
{
_reparent
(
scope
);
void
_visitChildren
(
void
visitor
(
FocusScopeNode
child
))
{
FocusScopeNode
child
=
_firstChild
;
while
(
child
!=
null
)
{
visitor
(
child
);
child
=
child
.
_nextSibling
;
}
assert
(
scope
.
ancestors
.
contains
(
this
));
// Move down the tree, checking each focusedChild until we get to a node
// that either isn't a scope node, or has no focused child, and then request
// focus on that node.
FocusNode
descendantFocus
=
scope
.
focusedChild
;
while
(
descendantFocus
is
FocusScopeNode
&&
descendantFocus
!=
null
)
{
final
FocusScopeNode
descendantScope
=
descendantFocus
;
descendantFocus
=
descendantScope
.
focusedChild
;
}
bool
_debugUltimatePreviousSiblingOf
(
FocusScopeNode
child
,
{
FocusScopeNode
equals
})
{
while
(
child
.
_previousSibling
!=
null
)
{
assert
(
child
.
_previousSibling
!=
child
);
child
=
child
.
_previousSibling
;
}
return
child
==
equals
;
}
bool
_debugUltimateNextSiblingOf
(
FocusScopeNode
child
,
{
FocusScopeNode
equals
})
{
while
(
child
.
_nextSibling
!=
null
)
{
assert
(
child
.
_nextSibling
!=
child
);
child
=
child
.
_nextSibling
;
}
return
child
==
equals
;
}
void
_remove
(
FocusScopeNode
child
)
{
assert
(
child
.
_parent
==
this
);
assert
(
child
.
_manager
==
_manager
);
assert
(
_debugUltimatePreviousSiblingOf
(
child
,
equals:
_firstChild
));
assert
(
_debugUltimateNextSiblingOf
(
child
,
equals:
_lastChild
));
if
(
child
.
_previousSibling
==
null
)
{
assert
(
_firstChild
==
child
);
_firstChild
=
child
.
_nextSibling
;
}
else
{
child
.
_previousSibling
.
_nextSibling
=
child
.
_nextSibling
;
}
if
(
descendantFocus
!=
null
)
{
descendantFocus
?.
_doRequestFocus
(
isFromPolicy:
false
);
if
(
child
.
_nextSibling
==
null
)
{
assert
(
_lastChild
==
child
);
_lastChild
=
child
.
_previousSibling
;
}
else
{
scope
.
_doRequestFocus
(
isFromPolicy:
false
)
;
child
.
_nextSibling
.
_previousSibling
=
child
.
_previousSibling
;
}
child
.
_previousSibling
=
null
;
child
.
_nextSibling
=
null
;
child
.
_parent
=
null
;
child
.
_updateManager
(
null
);
}
/// If this scope lacks a focus, request that the given node become the focus.
void
_didChangeFocusChain
()
{
if
(
isFirstFocus
)
_manager
?.
_markNeedsUpdate
();
}
/// Requests that the given node becomes the focus for this scope.
///
/// If the given node is not yet part of the focus tree, then add it as a
/// child of this node.
/// If the given node is currently focused in another scope, the node will
/// first be unfocused in that scope.
///
/// The node will receive the overall focus if this [isFirstFocus] is true
/// in this scope and all its ancestor scopes. The node is notified that it
/// has received the overall focus in a microtask.
void
requestFocus
(
FocusNode
node
)
{
assert
(
node
!=
null
);
if
(
_focus
==
node
&&
listEquals
<
FocusScopeNode
>(
_focusPath
,
_manager
?.
_getCurrentFocusPath
()))
return
;
_focus
?.
unfocus
();
node
.
_hasKeyboardToken
=
true
;
_setFocus
(
node
);
}
/// If this scope lacks a focus, request that the given node becomes the
/// focus.
///
/// Useful for widgets that wish to grab the focus if no other widget already
/// has the focus.
///
/// The node is notified that it has received the
primary
focus in a
/// microtask
, so notification may lag the request by up to one frame
.
/// The node is notified that it has received the
overall
focus in a
/// microtask.
void
autofocus
(
FocusNode
node
)
{
if
(
focusedChild
==
null
)
{
if
(
node
.
_parent
==
null
)
{
_reparent
(
node
);
}
assert
(
node
.
ancestors
.
contains
(
this
),
'Autofocus was requested for a node that is not a descendant of the scope from which it was requested.'
);
node
.
requestFocus
();
assert
(
node
!=
null
);
if
(
_focus
==
null
)
{
node
.
_hasKeyboardToken
=
true
;
_setFocus
(
node
);
}
}
@override
void
_doRequestFocus
({
@required
bool
isFromPolicy
})
{
assert
(
isFromPolicy
!=
null
);
// Start with the primary focus as the focused child of this scope, if there
// is one. Otherwise start with this node itself.
FocusNode
primaryFocus
=
focusedChild
??
this
;
// Keep going down through scopes until the ultimately focusable item is
// found, a scope doesn't have a focusedChild, or a non-scope is
// encountered.
while
(
primaryFocus
is
FocusScopeNode
&&
primaryFocus
.
focusedChild
!=
null
)
{
final
FocusScopeNode
scope
=
primaryFocus
;
primaryFocus
=
scope
.
focusedChild
;
}
if
(
primaryFocus
is
FocusScopeNode
)
{
// We didn't find a FocusNode at the leaf, so we're focusing the scope.
_markAsDirty
(
newFocus:
primaryFocus
);
/// Adopts the given node if it is focused in another scope.
///
/// A widget that requests that a node is focused should call this method
/// during its `build` method in case the widget is moved from one location
/// in the tree to another location that has a different focus scope.
void
reparentIfNeeded
(
FocusNode
node
)
{
assert
(
node
!=
null
);
if
(
node
.
_parent
==
null
||
node
.
_parent
==
this
)
return
;
node
.
unfocus
();
assert
(
node
.
_parent
==
null
);
if
(
_focus
==
null
)
_setFocus
(
node
);
}
void
_setFocus
(
FocusNode
node
)
{
assert
(
node
!=
null
);
assert
(
node
.
_parent
==
null
);
assert
(
_focus
==
null
);
_focus
=
node
;
_focus
.
_parent
=
this
;
_focus
.
_manager
=
_manager
;
_focus
.
_hasKeyboardToken
=
true
;
_focusPath
=
_getFocusPath
();
_didChangeFocusChain
();
}
void
_resignFocus
(
FocusNode
node
)
{
assert
(
node
!=
null
);
if
(
_focus
!=
node
)
return
;
_focus
.
_parent
=
null
;
_focus
.
_manager
=
null
;
_focus
=
null
;
_didChangeFocusChain
();
}
/// Makes the given child the first focus of this scope.
///
/// If the child has another parent scope, the child is first removed from
/// that scope. After this method returns [isFirstFocus] will be true for
/// the child.
void
setFirstFocus
(
FocusScopeNode
child
)
{
assert
(
child
!=
null
);
if
(
_firstChild
==
child
)
return
;
child
.
detach
();
_prepend
(
child
);
assert
(
child
.
_parent
==
this
);
_didChangeFocusChain
();
}
/// Adopts the given scope if it is the first focus of another scope.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during its `build` method in case the widget is moved
/// from one location in the tree to another location that has a different
/// focus scope.
///
/// If the given scope is not the first focus of its old parent, the scope
/// is simply detached from its old parent.
void
reparentScopeIfNeeded
(
FocusScopeNode
child
)
{
assert
(
child
!=
null
);
if
(
child
.
_parent
==
null
||
child
.
_parent
==
this
)
return
;
if
(
child
.
isFirstFocus
)
{
setFirstFocus
(
child
);
}
else
{
primaryFocus
.
requestFocus
();
child
.
detach
();
}
}
/// Remove this scope from its parent child list.
///
/// This method is safe to call even if this scope does not have a parent.
///
/// A widget that sets a scope as the first focus of another scope should
/// call this method during [State.dispose] to avoid leaving dangling
/// children in their parent scope.
void
detach
()
{
_didChangeFocusChain
();
_parent
?.
_remove
(
this
);
assert
(
_parent
==
null
);
}
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focusedChild'
,
focusedChild
,
defaultValue:
null
));
if
(
_focus
!=
null
)
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'focus'
,
_focus
));
}
@override
List
<
DiagnosticsNode
>
debugDescribeChildren
()
{
final
List
<
DiagnosticsNode
>
children
=
<
DiagnosticsNode
>[];
if
(
_firstChild
!=
null
)
{
FocusScopeNode
child
=
_firstChild
;
int
count
=
1
;
while
(
true
)
{
children
.
add
(
child
.
toDiagnosticsNode
(
name:
'child
$count
'
));
if
(
child
==
_lastChild
)
break
;
child
=
child
.
_nextSibling
;
count
+=
1
;
}
}
return
children
;
}
}
...
...
@@ -892,175 +418,70 @@ class FocusScopeNode extends FocusNode {
///
/// See also:
///
/// * [FocusNode], which is a
node in the focus tree that can receive focus.
///
* [FocusScopeNode], which is an node in the focus tree used to collect
///
subtrees into groups
.
/// * [Focus
.of], which provides the nearest ancestor [Focus
Node] for a given
/// * [FocusNode], which is a
leaf node in the focus tree that can receive
///
focus.
///
* [FocusScopeNode], which is an interior node in the focus tree
.
/// * [Focus
Scope.of], which provides the [FocusScope
Node] for a given
/// [BuildContext].
/// * [FocusScope.of], which provides the nearest ancestor [FocusScopeNode] for
/// a given [BuildContext].
class
FocusManager
with
DiagnosticableTreeMixin
{
class
FocusManager
{
/// Creates an object that manages the focus tree.
///
/// This constructor is rarely called directly. To access the [FocusManager],
/// consider using [WidgetsBinding.focusManager] instead.
FocusManager
()
{
rootScope
.
_manager
=
this
;
RawKeyboard
.
instance
.
addListener
(
_handleRawKeyEvent
);
assert
(
rootScope
.
_firstChild
==
null
);
assert
(
rootScope
.
_lastChild
==
null
);
}
/// The root [FocusScopeNode] in the focus tree.
///
/// This field is rarely used directly. To find the nearest [FocusScopeNode]
/// for a given [FocusNode], call [FocusNode.nearestScope].
FocusScopeNode
get
rootScope
=>
_rootScope
;
FocusScopeNode
_rootScope
=
FocusScopeNode
(
debugLabel:
'Root Focus Scope'
);
/// This field is rarely used directly. Instead, to find the
/// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of].
final
FocusScopeNode
rootScope
=
FocusScopeNode
();
void
_handleRawKeyEvent
(
RawKeyEvent
event
)
{
// Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it, stop.
if
(
_currentFocus
==
null
)
{
return
;
}
Iterable
<
FocusNode
>
allNodes
(
FocusNode
node
)
sync
*
{
yield
node
;
for
(
FocusNode
ancestor
in
node
.
ancestors
)
{
yield
ancestor
;
}
}
for
(
FocusNode
node
in
allNodes
(
_currentFocus
))
{
if
(
node
.
onKey
!=
null
&&
node
.
onKey
(
node
,
event
))
{
break
;
}
}
}
/// Resets the FocusManager to a base state.
///
/// This is used by test infrastructure to reset the state between tests.
/// It is not meant for regular production use.
void
reset
()
{
_currentFocus
=
null
;
_nextFocus
=
null
;
_haveScheduledUpdate
=
false
;
_rootScope
=
FocusScopeNode
(
debugLabel:
'Root Focus Scope'
);
rootScope
.
_manager
=
this
;
}
// The node that currently has the primary focus.
FocusNode
_currentFocus
;
// The node that has requested to have the primary focus, but hasn't been
// given it yet.
FocusNode
_nextFocus
;
// The set of nodes that need to notify their listeners of changes at the next
// update.
final
Set
<
FocusNode
>
_dirtyNodes
=
<
FocusNode
>{};
// Called to indicate that the given node is being disposed.
void
_willDisposeFocusNode
(
FocusNode
node
)
{
assert
(
node
!=
null
);
_willUnfocusNode
(
node
);
_dirtyNodes
.
remove
(
node
);
}
// Called to indicate that the given node is being unfocused, and that any
// pending request to be focused should be canceled.
void
_willUnfocusNode
(
FocusNode
node
)
{
assert
(
node
!=
null
);
if
(
_currentFocus
==
node
)
{
if
(
_currentFocus
==
node
)
_currentFocus
=
null
;
_dirtyNodes
.
add
(
node
);
_markNeedsUpdate
();
}
if
(
_nextFocus
==
node
)
{
_nextFocus
=
null
;
_dirtyNodes
.
add
(
node
);
_markNeedsUpdate
();
}
}
// True indicates that there is an update pending.
bool
_haveScheduledUpdate
=
false
;
// Request that an update be scheduled, optionally requesting focus for the
// given newFocus node.
void
_markNeedsUpdate
({
FocusNode
newFocus
})
{
// If newFocus isn't specified, then don't mess with _nextFocus, just
// schedule the update.
_nextFocus
=
newFocus
??
_nextFocus
;
if
(
_haveScheduledUpdate
)
{
void
_markNeedsUpdate
()
{
if
(
_haveScheduledUpdate
)
return
;
}
_haveScheduledUpdate
=
true
;
scheduleMicrotask
(
_
applyFocusChang
e
);
scheduleMicrotask
(
_
updat
e
);
}
void
_applyFocusChange
()
{
FocusNode
_findNextFocus
()
{
FocusScopeNode
scope
=
rootScope
;
while
(
scope
.
_firstChild
!=
null
)
scope
=
scope
.
_firstChild
;
return
scope
.
_focus
;
}
void
_update
()
{
_haveScheduledUpdate
=
false
;
final
FocusNode
nextFocus
=
_findNextFocus
();
if
(
_currentFocus
==
nextFocus
)
return
;
final
FocusNode
previousFocus
=
_currentFocus
;
if
(
_currentFocus
==
null
&&
_nextFocus
==
null
)
{
_nextFocus
=
rootScope
;
}
if
(
_nextFocus
!=
null
&&
_nextFocus
!=
_currentFocus
)
{
_currentFocus
=
_nextFocus
;
final
Set
<
FocusNode
>
previousPath
=
previousFocus
?.
ancestors
?.
toSet
()
??
<
FocusNode
>{};
final
Set
<
FocusNode
>
nextPath
=
_nextFocus
.
ancestors
.
toSet
();
// Notify nodes that are newly focused.
_dirtyNodes
.
addAll
(
nextPath
.
difference
(
previousPath
));
// Notify nodes that are no longer focused
_dirtyNodes
.
addAll
(
previousPath
.
difference
(
nextPath
));
_nextFocus
=
null
;
}
if
(
previousFocus
!=
_currentFocus
)
{
if
(
previousFocus
!=
null
)
{
_dirtyNodes
.
add
(
previousFocus
);
}
if
(
_currentFocus
!=
null
)
{
_dirtyNodes
.
add
(
_currentFocus
);
}
}
for
(
FocusNode
node
in
_dirtyNodes
)
{
node
.
_notify
();
}
_dirtyNodes
.
clear
();
_currentFocus
=
nextFocus
;
previousFocus
?.
_notify
();
_currentFocus
?.
_notify
();
}
@override
List
<
DiagnosticsNode
>
debugDescribeChildren
()
{
return
<
DiagnosticsNode
>[
rootScope
.
toDiagnosticsNode
(
name:
'rootScope'
),
];
}
List
<
FocusScopeNode
>
_getCurrentFocusPath
()
=>
_currentFocus
?.
_parent
?.
_getFocusPath
();
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
properties
.
add
(
FlagProperty
(
'haveScheduledUpdate'
,
value:
_haveScheduledUpdate
,
ifTrue:
'UPDATE SCHEDULED'
));
properties
.
add
(
DiagnosticsProperty
<
FocusNode
>(
'currentFocus'
,
_currentFocus
,
defaultValue:
null
));
String
toString
()
{
final
String
status
=
_haveScheduledUpdate
?
' UPDATE SCHEDULED'
:
''
;
const
String
indent
=
' '
;
return
'
${describeIdentity(this)}$status
\n
'
'
${indent}
currentFocus:
$_currentFocus
\n
'
'
${rootScope.toStringDeep(prefixLineOne: indent, prefixOtherLines: indent)}
'
;
}
}
/// Returns a text representation of the current focus tree, along with the
/// current attributes on each node.
///
/// Will return an empty string in release builds.
String
debugDescribeFocusTree
(
)
{
assert
(
WidgetsBinding
.
instance
!=
null
);
String
result
;
assert
(()
{
result
=
WidgetsBinding
.
instance
.
focusManager
.
toStringDeep
();
return
true
;
}());
return
result
??
''
;
}
/// Prints a text representation of the current focus tree, along with the
/// current attributes on each node.
///
/// Will do nothing in release builds.
void
debugDumpFocusTree
(
)
{
assert
(()
{
debugPrint
(
debugDescribeFocusTree
());
return
true
;
}());
}
packages/flutter/lib/src/widgets/focus_scope.dart
View file @
590cc27b
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/foundation.dart'
;
import
'basic.dart'
;
import
'focus_manager.dart'
;
import
'framework.dart'
;
import
'inherited_notifier.dart'
;
/// A widget that manages a [FocusNode] to allow keyboard focus to be given
/// to this widget and its descendants.
///
/// When the focus is gained or lost, [onFocusChanged] is called.
///
/// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for
/// this widget's [focusNode], unless a focused descendant's [onKey] callback
/// returns false when called.
///
/// This widget does not provide any visual indication that the focus has
/// changed. Any desired visual changes should be made when [onFocusChanged] is
/// called.
///
/// To access the [FocusNode] of the nearest ancestor [Focus] widget and
/// establish a relationship that will rebuild the widget when the focus
/// changes, use the [Focus.of] and [FocusScope.of] static methods.
///
/// To access the focused state of the nearest [Focus] widget, use
/// [Focus.hasFocus] from a build method, which also establishes a relationship
/// between the calling widget and the [Focus] widget that will rebuild the
/// calling widget when the focus changes.
///
/// Managing a [FocusNode] means managing its lifecycle, listening for changes
/// in focus, and re-parenting it when needed to keep the focus hierarchy in
/// sync with the widget hierarchy. See [FocusNode] for more information about
/// the details of what node management entails if not using a [Focus] widget.
///
/// To collect a sub-tree of nodes into a group, use a [FocusScope].
class
_FocusScopeMarker
extends
InheritedWidget
{
const
_FocusScopeMarker
({
Key
key
,
@required
this
.
node
,
Widget
child
,
})
:
assert
(
node
!=
null
),
super
(
key:
key
,
child:
child
);
final
FocusScopeNode
node
;
@override
bool
updateShouldNotify
(
_FocusScopeMarker
oldWidget
)
{
return
node
!=
oldWidget
.
node
;
}
}
/// Establishes a scope in which widgets can receive focus.
///
/// {@tool snippet --template=stateful_widget_scaffold}
/// This example shows how to manage focus using the [Focus] and [FocusScope]
/// widgets. See [FocusNode] for a similar example that doesn't use [Focus] or
/// [FocusScope].
/// The focus tree keeps track of which widget is the user's current focus. The
/// focused widget often listens for keyboard events.
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ```
/// A focus scope does not itself receive focus but instead helps remember
/// previous focus states. A scope is currently active when its [node] is the
/// first focus of its parent scope. To activate a [FocusScope], either use the
/// [autofocus] property or explicitly make the [node] the first focus in the
/// parent scope:
///
/// ```dart
/// Color _color = Colors.white;
///
/// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// if (event is RawKeyDownEvent) {
/// print('Focus node ${node.debugLabel} got key event: ${event.logicalKey}');
/// if (event.logicalKey == LogicalKeyboardKey.keyR) {
/// print('Changing color to red.');
/// setState(() {
/// _color = Colors.red;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyG) {
/// print('Changing color to green.');
/// setState(() {
/// _color = Colors.green;
/// });
/// return true;
/// } else if (event.logicalKey == LogicalKeyboardKey.keyB) {
/// print('Changing color to blue.');
/// setState(() {
/// _color = Colors.blue;
/// });
/// return true;
/// }
/// }
/// return false;
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// final TextTheme textTheme = Theme.of(context).textTheme;
/// return FocusScope(
/// debugLabel: 'Scope',
/// autofocus: true,
/// child: DefaultTextStyle(
/// style: textTheme.display1,
/// child: Focus(
/// onKey: _handleKeyPress,
/// debugLabel: 'Button',
/// child: Builder(
/// builder: (BuildContext context) {
/// final FocusNode focusNode = Focus.of(context);
/// final bool hasFocus = focusNode.hasFocus;
/// return GestureDetector(
/// onTap: () {
/// if (hasFocus) {
/// setState(() {
/// focusNode.unfocus();
/// });
/// } else {
/// setState(() {
/// focusNode.requestFocus();
/// });
/// }
/// },
/// child: Center(
/// child: Container(
/// width: 400,
/// height: 100,
/// alignment: Alignment.center,
/// color: hasFocus ? _color : Colors.white,
/// child: Text(hasFocus ? "I'm in color! Press R,G,B!" : 'Press to focus'),
/// ),
/// ),
/// );
/// },
/// ),
/// ),
/// ),
/// );
/// }
/// FocusScope.of(context).setFirstFocus(node);
/// ```
/// {@end-tool}
///
/// If a [FocusScope] is removed from the widget tree, then the previously
/// focused node will be focused, but only if the [node] is the same [node]
/// object as in the previous frame. To assure this, you can use a GlobalKey to
/// keep the [FocusScope] widget from being rebuilt from one frame to the next,
/// or pass in the [node] from a parent that is not rebuilt. If there is no next
/// sibling, then the parent scope node will be focused.
///
/// See also:
///
/// * [FocusNode], which represents a node in the focus hierarchy and
/// [FocusNode]'s API documentation includes a detailed explanation of its
/// role in the overall focus system.
/// * [FocusScope], a widget that manages a group of focusable widgets using a
/// [FocusScopeNode].
/// * [FocusScopeNode], a node that collects focus nodes into a group for
/// traversal.
/// * [FocusManager], a singleton that manages the primary focus and
/// distributes key events to focused nodes.
class
Focus
extends
StatefulWidget
{
/// Creates a widget that manages a [FocusNode].
///
/// The [child] argument is required and must not be null.
/// * [FocusScopeNode], which is the associated node in the focus tree.
/// * [FocusNode], which is a leaf node in the focus tree that can receive
/// focus.
class
FocusScope
extends
StatefulWidget
{
/// Creates a scope in which widgets can receive focus.
///
/// The [
autofocus
] argument must not be null.
const
Focus
({
/// The [
node
] argument must not be null.
const
Focus
Scope
({
Key
key
,
@required
this
.
child
,
this
.
focusNode
,
@required
this
.
node
,
this
.
autofocus
=
false
,
this
.
onFocusChange
,
this
.
onKey
,
this
.
debugLabel
,
})
:
assert
(
child
!=
null
),
assert
(
autofocus
!=
null
),
super
(
key:
key
);
/// A debug label for this widget.
///
/// Not used for anything except to be printed in the diagnostic output from
/// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
/// since that node can have its own [FocusNode.debugLabel].
///
/// To get a string with the entire tree, call [debugDescribeFocusTree]. To
/// print it to the console call [debugDumpFocusTree].
///
/// Defaults to null.
final
String
debugLabel
;
/// The child widget of this [Focus].
///
/// {@macro flutter.widgets.child}
final
Widget
child
;
this
.
child
,
})
:
assert
(
node
!=
null
),
assert
(
autofocus
!=
null
),
super
(
key:
key
);
/// Handler for keys pressed when this object or one of its children has
/// focus.
///
/// Key events are first given to the [FocusNode] that has primary focus, and
/// if its [onKey] method return false, then they are given to each ancestor
/// node up the focus hierarchy in turn. If an event reaches the root of the
/// hierarchy, it is discarded.
///
/// This is not the way to get text input in the manner of a text field: it
/// leaves out support for input method editors, and doesn't support soft
/// keyboards in general. For text input, consider [TextField],
/// [EditableText], or [CupertinoTextField] instead, which do support these
/// things.
final
FocusOnKeyCallback
onKey
;
/// Controls whether this scope is currently active.
final
FocusScopeNode
node
;
/// Handler called when the focus changes.
///
/// Called with true if this node gains focus, and false if it loses
/// focus.
final
ValueChanged
<
bool
>
onFocusChange
;
/// True if this widget will be selected as the initial focus when no other
/// node in its scope is currently focused.
///
/// Ideally, there is only one [Focus] with autofocus set in each
/// [FocusScope]. If there is more than one [Focus] with autofocus set, then
/// the first one added to the tree will get focus.
/// Whether this scope should attempt to become active when first added to
/// the tree.
final
bool
autofocus
;
///
An optional focus node to use as the focus node for this [Focus] widget
.
///
The widget below this widget in the tree
.
///
/// If one is not supplied, then one will be allocated and owned by this
/// widget.
///
/// Supplying a focus node is sometimes useful if an ancestor to this widget
/// wants to control when this widget has the focus. The owner will be
/// responsible for calling [FocusNode.dispose] on the focus node when it is
/// done with it, but this [Focus] widget will attach/detach and reparent the
/// node when needed.
final
FocusNode
focusNode
;
/// {@macro flutter.widgets.child}
final
Widget
child
;
/// Returns the [focusNode] of the [Focus] that most tightly encloses the given
/// [BuildContext].
///
/// If this node doesn't have a [Focus] widget ancestor, then the
/// [FocusManager.rootScope] is returned.
/// Returns the [node] of the [FocusScope] that most tightly encloses the
/// given [BuildContext].
///
/// The [context] argument must not be null.
static
FocusNode
of
(
BuildContext
context
)
{
static
Focus
Scope
Node
of
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
_Focus
Marker
marker
=
context
.
inheritFromWidgetOfExactType
(
_Focus
Marker
);
return
marker
?.
notifier
??
context
.
owner
.
focusManager
.
rootScope
;
final
_Focus
ScopeMarker
scope
=
context
.
inheritFromWidgetOfExactType
(
_FocusScope
Marker
);
return
scope
?.
node
??
context
.
owner
.
focusManager
.
rootScope
;
}
/// Returns true if the nearest enclosing [Focus] widget's node is focused.
/// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of
/// the given [BuildContext]. The first element of the list is the
/// nearest ancestor's [FocusScopeNode].
///
/// A convenience method to allow build methods to write:
/// `Focus.isAt(context)` to get whether or not the nearest [Focus] or
/// [FocusScope] above them in the widget hierarchy currently has the keyboard
/// focus.
static
bool
isAt
(
BuildContext
context
)
=>
Focus
.
of
(
context
).
hasFocus
;
@override
void
debugFillProperties
(
DiagnosticPropertiesBuilder
properties
)
{
super
.
debugFillProperties
(
properties
);
properties
.
add
(
StringProperty
(
'debugLabel'
,
debugLabel
,
defaultValue:
null
));
properties
.
add
(
FlagProperty
(
'autofocus'
,
value:
autofocus
,
ifTrue:
'AUTOFOCUS'
,
defaultValue:
false
));
properties
.
add
(
DiagnosticsProperty
<
FocusScopeNode
>(
'node'
,
focusNode
,
defaultValue:
null
));
/// The returned list does not include the [FocusManager]'s `rootScope`.
/// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are
/// returned.
///
/// The [context] argument must not be null.
static
List
<
FocusScopeNode
>
ancestorsOf
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
List
<
FocusScopeNode
>
ancestors
=
<
FocusScopeNode
>[];
while
(
true
)
{
context
=
context
.
ancestorInheritedElementForWidgetOfExactType
(
_FocusScopeMarker
);
if
(
context
==
null
)
return
ancestors
;
final
_FocusScopeMarker
scope
=
context
.
widget
;
ancestors
.
add
(
scope
.
node
);
context
.
visitAncestorElements
((
Element
parent
)
{
context
=
parent
;
return
false
;
});
}
}
@override
_FocusS
tate
createState
()
=>
_Focus
State
();
_FocusS
copeState
createState
()
=>
_FocusScope
State
();
}
class
_FocusState
extends
State
<
Focus
>
{
FocusNode
_internalNode
;
FocusNode
get
node
=>
widget
.
focusNode
??
_internalNode
;
bool
_hasFocus
;
class
_FocusScopeState
extends
State
<
FocusScope
>
{
bool
_didAutofocus
=
false
;
FocusAttachment
_focusAttachment
;
@override
void
initState
()
{
super
.
initState
();
_initNode
();
}
void
_initNode
()
{
if
(
widget
.
focusNode
==
null
)
{
// Only create a new node if the widget doesn't have one.
_internalNode
??=
_createNode
();
}
_focusAttachment
=
node
.
attach
(
context
,
onKey:
widget
.
onKey
);
_hasFocus
=
node
.
hasFocus
;
// Add listener even if the _internalNode existed before, since it should
// not be listening now if we're re-using a previous one, because it should
// have already removed its listener.
node
.
addListener
(
_handleFocusChanged
);
}
FocusNode
_createNode
()
{
return
FocusNode
(
debugLabel:
widget
.
debugLabel
,
);
}
@override
void
dispose
()
{
// Regardless of the node owner, we need to remove it from the tree and stop
// listening to it.
node
.
removeListener
(
_handleFocusChanged
);
_focusAttachment
.
detach
();
// Don't manage the lifetime of external nodes given to the widget, just the
// internal node.
_internalNode
?.
dispose
();
super
.
dispose
();
}
@override
void
didChangeDependencies
()
{
super
.
didChangeDependencies
();
_focusAttachment
?.
reparent
();
if
(!
_didAutofocus
&&
widget
.
autofocus
)
{
FocusScope
.
of
(
context
).
autofocus
(
node
);
FocusScope
.
of
(
context
).
setFirstFocus
(
widget
.
node
);
_didAutofocus
=
true
;
}
}
@override
void
didUpdateWidget
(
Focus
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
if
(
oldWidget
.
debugLabel
!=
widget
.
debugLabel
&&
_internalNode
!=
null
)
{
_internalNode
.
debugLabel
=
widget
.
debugLabel
;
}
if
((
oldWidget
.
focusNode
==
widget
.
focusNode
&&
oldWidget
.
onKey
==
widget
.
onKey
)
||
oldWidget
.
focusNode
==
null
&&
widget
.
focusNode
==
null
)
{
// Either there aren't changes, or the _internalNode is already attached
// and being listened to.
return
;
}
_focusAttachment
.
detach
();
if
(
oldWidget
.
focusNode
==
null
&&
widget
.
focusNode
!=
null
)
{
// We're no longer using the node we were managing. We don't stop managing
// it until dispose, so just detach it: we might re-use it eventually, and
// calling dispose on it here will confuse other widgets that haven't yet
// been notified of a widget change and might still be listening.
_internalNode
?.
removeListener
(
_handleFocusChanged
);
_focusAttachment
=
widget
.
focusNode
?.
attach
(
context
,
onKey:
widget
.
onKey
);
widget
.
focusNode
?.
addListener
(
_handleFocusChanged
);
}
else
if
(
oldWidget
.
focusNode
!=
null
&&
widget
.
focusNode
==
null
)
{
oldWidget
.
focusNode
?.
removeListener
(
_handleFocusChanged
);
// We stopped using the external node, and now we need to manage one.
_initNode
();
}
else
{
// We just switched which node the widget had, so just change what we
// listen to/attach.
oldWidget
.
focusNode
.
removeListener
(
_handleFocusChanged
);
widget
.
focusNode
.
addListener
(
_handleFocusChanged
);
_focusAttachment
=
widget
.
focusNode
.
attach
(
context
,
onKey:
widget
.
onKey
);
}
_hasFocus
=
node
.
hasFocus
;
}
void
_handleFocusChanged
()
{
if
(
_hasFocus
!=
node
.
hasFocus
)
{
setState
(()
{
_hasFocus
=
node
.
hasFocus
;
});
if
(
widget
.
onFocusChange
!=
null
)
{
widget
.
onFocusChange
(
node
.
hasFocus
);
}
}
}
@override
Widget
build
(
BuildContext
context
)
{
_focusAttachment
.
reparent
();
return
_FocusMarker
(
node:
node
,
child:
widget
.
child
,
);
}
}
/// A [FocusScope] is similar to a [Focus], but also serves as a scope for other
/// [Focus]s and [FocusScope]s, grouping them together.
///
/// Like [Focus], [FocusScope] provides an [onFocusChange] as a way to be
/// notified when the focus is given to or removed from this widget.
///
/// The [onKey] argument allows specification of a key event handler that is
/// invoked when this node or one of its children has focus. Keys are handed to
/// the primary focused widget first, and then they propagate through the
/// ancestors of that node, stopping if one of them returns true from [onKey],
/// indicating that it has handled the event.
///
/// A [FocusScope] manages a [FocusScopeNode]. Managing a [FocusScopeNode] means
/// managing its lifecycle, listening for changes in focus, and re-parenting it
/// when the widget hierarchy changes. See [FocusNode] and [FocusScopeNode] for
/// more information about the details of what node management entails if not
/// using a [FocusScope] widget.
///
/// See also:
///
/// * [FocusScopeNode], which represents a scope node in the focus hierarchy.
/// * [FocusNode], which represents a node in the focus hierarchy and has an
/// explanation of the focus system.
/// * [Focus], a widget that manages a [FocusNode] and allows easy access to
/// managing focus without having to manage the node.
/// * [FocusManager], a singleton that manages the focus and distributes key
/// events to focused nodes.
class
FocusScope
extends
Focus
{
/// Creates a widget that manages a [FocusScopeNode].
///
/// The [child] argument is required and must not be null.
///
/// The [autofocus], and [showDecorations] arguments must not be null.
const
FocusScope
({
Key
key
,
FocusNode
node
,
@required
Widget
child
,
bool
autofocus
=
false
,
ValueChanged
<
bool
>
onFocusChange
,
FocusOnKeyCallback
onKey
,
String
debugLabel
,
})
:
assert
(
child
!=
null
),
assert
(
autofocus
!=
null
),
super
(
key:
key
,
child:
child
,
focusNode:
node
,
autofocus:
autofocus
,
onFocusChange:
onFocusChange
,
onKey:
onKey
,
debugLabel:
debugLabel
,
);
/// Returns the [FocusScopeNode] of the [FocusScope] that most tightly
/// encloses the given [context].
///
/// If this node doesn't have a [Focus] widget ancestor, then the
/// [FocusManager.rootScope] is returned.
///
/// The [context] argument must not be null.
static
FocusScopeNode
of
(
BuildContext
context
)
{
assert
(
context
!=
null
);
final
_FocusMarker
marker
=
context
.
inheritFromWidgetOfExactType
(
_FocusMarker
);
return
marker
?.
notifier
?.
nearestScope
??
context
.
owner
.
focusManager
.
rootScope
;
}
@override
_FocusScopeState
createState
()
=>
_FocusScopeState
();
}
class
_FocusScopeState
extends
_FocusState
{
@override
FocusScopeNode
_createNode
()
{
return
FocusScopeNode
(
debugLabel:
widget
.
debugLabel
,
);
void
dispose
()
{
widget
.
node
.
detach
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
_focusAttachment
.
reparent
(
);
FocusScope
.
of
(
context
).
reparentScopeIfNeeded
(
widget
.
node
);
return
Semantics
(
explicitChildNodes:
true
,
child:
_FocusMarker
(
node:
node
,
child:
_Focus
Scope
Marker
(
node:
widget
.
node
,
child:
widget
.
child
,
),
);
}
}
// The InheritedWidget marker for Focus and FocusScope.
class
_FocusMarker
extends
InheritedNotifier
<
FocusNode
>
{
const
_FocusMarker
({
Key
key
,
@required
FocusNode
node
,
@required
Widget
child
,
})
:
assert
(
node
!=
null
),
assert
(
child
!=
null
),
super
(
key:
key
,
notifier:
node
,
child:
child
);
}
packages/flutter/lib/src/widgets/framework.dart
View file @
590cc27b
...
...
@@ -2124,7 +2124,7 @@ class BuildOwner {
/// the [FocusScopeNode] for a given [BuildContext].
///
/// See [FocusManager] for more details.
FocusManager
focusManager
=
FocusManager
();
final
FocusManager
focusManager
=
FocusManager
();
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
...
...
packages/flutter/lib/src/widgets/navigator.dart
View file @
590cc27b
...
...
@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final
Set
<
Route
<
dynamic
>>
_poppedRoutes
=
<
Route
<
dynamic
>>{};
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
(
debugLabel:
'Navigator Scope'
);
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
();
final
List
<
OverlayEntry
>
_initialOverlayEntries
=
<
OverlayEntry
>[];
...
...
@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route
.
dispose
();
_poppedRoutes
.
clear
();
_history
.
clear
();
focusScopeNode
.
d
ispose
();
focusScopeNode
.
d
etach
();
super
.
dispose
();
assert
(()
{
_debugLocked
=
false
;
return
true
;
}());
}
...
...
packages/flutter/lib/src/widgets/raw_keyboard_listener.dart
View file @
590cc27b
...
...
@@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
import
'basic.dart'
;
import
'focus_manager.dart'
;
import
'focus_scope.dart'
;
import
'framework.dart'
;
export
'package:flutter/services.dart'
show
RawKeyEvent
;
...
...
@@ -113,5 +112,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
}
@override
Widget
build
(
BuildContext
context
)
=>
Focus
(
focusNode:
widget
.
focusNode
,
child:
widget
.
child
)
;
Widget
build
(
BuildContext
context
)
=>
widget
.
child
;
}
packages/flutter/lib/src/widgets/routes.dart
View file @
590cc27b
...
...
@@ -583,9 +583,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
// This is the combination of the two animations for the route.
Listenable
_listenable
;
/// The node this scope will use for its root [FocusScope] widget.
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
(
debugLabel:
'
$_ModalScopeState
Focus Scope'
);
@override
void
initState
()
{
super
.
initState
();
...
...
@@ -595,14 +592,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
if
(
widget
.
route
.
secondaryAnimation
!=
null
)
animations
.
add
(
widget
.
route
.
secondaryAnimation
);
_listenable
=
Listenable
.
merge
(
animations
);
widget
.
route
.
_grabFocusIfNeeded
(
focusScopeNode
);
}
@override
void
didUpdateWidget
(
_ModalScope
<
T
>
oldWidget
)
{
super
.
didUpdateWidget
(
oldWidget
);
assert
(
widget
.
route
==
oldWidget
.
route
);
widget
.
route
.
_grabFocusIfNeeded
(
focusScopeNode
);
}
@override
...
...
@@ -617,12 +612,6 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
});
}
@override
void
dispose
()
{
super
.
dispose
();
focusScopeNode
.
dispose
();
}
// This should be called to wrap any changes to route.isCurrent, route.canPop,
// and route.offstage.
void
_routeSetState
(
VoidCallback
fn
)
{
...
...
@@ -640,7 +629,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
child:
PageStorage
(
bucket:
widget
.
route
.
_storageBucket
,
// immutable
child:
FocusScope
(
node:
focusScopeNode
,
// immutable
node:
widget
.
route
.
focusScopeNode
,
// immutable
child:
RepaintBoundary
(
child:
AnimatedBuilder
(
animation:
_listenable
,
// immutable
...
...
@@ -898,6 +887,9 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return
child
;
}
/// The node this route will use for its root [FocusScope] widget.
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
();
@override
void
install
(
OverlayEntry
insertionPoint
)
{
super
.
install
(
insertionPoint
);
...
...
@@ -905,20 +897,18 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
_secondaryAnimationProxy
=
ProxyAnimation
(
super
.
secondaryAnimation
);
}
bool
_wantsFocus
=
false
;
void
_grabFocusIfNeeded
(
FocusScopeNode
node
)
{
if
(
_wantsFocus
)
{
_wantsFocus
=
false
;
navigator
.
focusScopeNode
.
setFirstFocus
(
node
);
}
}
@override
TickerFuture
didPush
()
{
_wantsFocus
=
true
;
navigator
.
focusScopeNode
.
setFirstFocus
(
focusScopeNode
)
;
return
super
.
didPush
();
}
@override
void
dispose
()
{
focusScopeNode
.
detach
();
super
.
dispose
();
}
// The API for subclasses to override - used by this class
/// Whether you can dismiss this route by tapping the modal barrier.
...
...
packages/flutter/test/cupertino/tab_scaffold_test.dart
View file @
590cc27b
...
...
@@ -106,10 +106,7 @@ void main() {
testWidgets
(
'Last tab gets focus'
,
(
WidgetTester
tester
)
async
{
// 2 nodes for 2 tabs
final
List
<
FocusNode
>
focusNodes
=
<
FocusNode
>[
FocusNode
(
debugLabel:
'Node 1'
),
FocusNode
(
debugLabel:
'Node 2'
),
];
final
List
<
FocusNode
>
focusNodes
=
<
FocusNode
>[
FocusNode
(),
FocusNode
()];
await
tester
.
pumpWidget
(
CupertinoApp
(
...
...
@@ -142,10 +139,7 @@ void main() {
testWidgets
(
'Do not affect focus order in the route'
,
(
WidgetTester
tester
)
async
{
final
List
<
FocusNode
>
focusNodes
=
<
FocusNode
>[
FocusNode
(
debugLabel:
'Node 1'
),
FocusNode
(
debugLabel:
'Node 2'
),
FocusNode
(
debugLabel:
'Node 3'
),
FocusNode
(
debugLabel:
'Node 4'
),
FocusNode
(),
FocusNode
(),
FocusNode
(),
FocusNode
(),
];
await
tester
.
pumpWidget
(
...
...
packages/flutter/test/material/text_field_focus_test.dart
View file @
590cc27b
...
...
@@ -9,14 +9,11 @@ void main() {
testWidgets
(
'Dialog interaction'
,
(
WidgetTester
tester
)
async
{
expect
(
tester
.
testTextInput
.
isVisible
,
isFalse
);
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Editable Text Node'
);
await
tester
.
pumpWidget
(
MaterialApp
(
const
MaterialApp
(
home:
Material
(
child:
Center
(
child:
TextField
(
focusNode:
focusNode
,
autofocus:
true
,
),
),
...
...
@@ -133,7 +130,7 @@ void main() {
await
tester
.
pumpWidget
(
Container
());
expect
(
tester
.
testTextInput
.
isVisible
,
isFalse
);
}
);
}
,
skip:
true
);
// https://github.com/flutter/flutter/issues/29384.
testWidgets
(
'Focus triggers keep-alive'
,
(
WidgetTester
tester
)
async
{
final
FocusNode
focusNode
=
FocusNode
();
...
...
packages/flutter/test/material/text_field_test.dart
View file @
590cc27b
...
...
@@ -2754,31 +2754,29 @@ void main() {
controller
=
TextEditingController
();
});
Future
<
void
>
setupWidget
(
WidgetTester
tester
)
async
{
MaterialApp
setupWidget
()
{
final
FocusNode
focusNode
=
FocusNode
();
controller
=
TextEditingController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Material
(
child:
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
null
,
child:
TextField
(
controller:
controller
,
maxLines:
3
,
strutStyle:
StrutStyle
.
disabled
,
),
return
MaterialApp
(
home:
Material
(
child:
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
null
,
child:
TextField
(
controller:
controller
,
maxLines:
3
,
strutStyle:
StrutStyle
.
disabled
,
),
),
)
,
),
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
}
testWidgets
(
'Shift test 1'
,
(
WidgetTester
tester
)
async
{
await
setupWidget
(
tester
);
await
tester
.
pumpWidget
(
setupWidget
());
const
String
testValue
=
'a big house'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -2791,7 +2789,7 @@ void main() {
});
testWidgets
(
'Control Shift test'
,
(
WidgetTester
tester
)
async
{
await
setupWidget
(
tester
);
await
tester
.
pumpWidget
(
setupWidget
()
);
const
String
testValue
=
'their big house'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -2807,7 +2805,7 @@ void main() {
});
testWidgets
(
'Down and up test'
,
(
WidgetTester
tester
)
async
{
await
setupWidget
(
tester
);
await
tester
.
pumpWidget
(
setupWidget
()
);
const
String
testValue
=
'a big house'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -2829,7 +2827,7 @@ void main() {
});
testWidgets
(
'Down and up test 2'
,
(
WidgetTester
tester
)
async
{
await
setupWidget
(
tester
);
await
tester
.
pumpWidget
(
setupWidget
()
);
const
String
testValue
=
'a big house
\n
jumped over a mouse
\n
One more line yay'
;
// 11 \n 19
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -2916,8 +2914,6 @@ void main() {
),
),
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
const
String
testValue
=
'a big house
\n
jumped over a mouse'
;
// 11 \n 19
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -2988,8 +2984,6 @@ void main() {
),
),
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
const
String
testValue
=
'a big house
\n
jumped over a mouse'
;
// 11 \n 19
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
@@ -3099,8 +3093,6 @@ void main() {
),
),
);
focusNode
.
requestFocus
();
await
tester
.
pump
();
const
String
testValue
=
'a big house
\n
jumped over a mouse'
;
// 11 \n 19
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
590cc27b
...
...
@@ -16,8 +16,8 @@ import 'editable_text_utils.dart';
import
'semantics_tester.dart'
;
final
TextEditingController
controller
=
TextEditingController
();
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'EditableText Node'
);
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
(
debugLabel:
'EditableText Scope Node'
);
final
FocusNode
focusNode
=
FocusNode
();
final
FocusScopeNode
focusScopeNode
=
FocusScopeNode
();
const
TextStyle
textStyle
=
TextStyle
();
const
Color
cursorColor
=
Color
.
fromARGB
(
0xFF
,
0xFF
,
0x00
,
0x00
);
...
...
@@ -975,9 +975,6 @@ void main() {
),
));
focusNode
.
requestFocus
();
await
tester
.
pump
();
expect
(
semantics
,
includesNodeWith
(
...
...
@@ -1535,8 +1532,6 @@ void main() {
),
);
focusNode
.
requestFocus
();
// Now change it to make it obscure text.
await
tester
.
pumpWidget
(
MaterialApp
(
home:
EditableText
(
...
...
@@ -1911,7 +1906,7 @@ void main() {
);
final
GlobalKey
<
EditableTextState
>
editableTextKey
=
GlobalKey
<
EditableTextState
>();
final
FocusNode
focusNode
=
FocusNode
(
debugLabel:
'Test Focus Node'
);
final
FocusNode
focusNode
=
FocusNode
();
await
tester
.
pumpWidget
(
MaterialApp
(
// So we can show overlays.
home:
EditableText
(
...
...
packages/flutter/test/widgets/focus_manager_test.dart
deleted
100644 → 0
View file @
096439b4
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:typed_data'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
sendFakeKeyEvent
(
Map
<
String
,
dynamic
>
data
)
{
BinaryMessages
.
handlePlatformMessage
(
SystemChannels
.
keyEvent
.
name
,
SystemChannels
.
keyEvent
.
codec
.
encodeMessage
(
data
),
(
ByteData
data
)
{},
);
}
void
main
(
)
{
final
GlobalKey
widgetKey
=
GlobalKey
();
Future
<
BuildContext
>
setupWidget
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
Container
(
key:
widgetKey
));
return
widgetKey
.
currentContext
;
}
group
(
FocusNode
,
()
{
testWidgets
(
'Can add children.'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusNode
parent
=
FocusNode
();
final
FocusAttachment
parentAttachment
=
parent
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
parentAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
child1Attachment
.
reparent
(
parent:
parent
);
expect
(
child1
.
parent
,
equals
(
parent
));
expect
(
parent
.
children
.
first
,
equals
(
child1
));
expect
(
parent
.
children
.
last
,
equals
(
child1
));
child2Attachment
.
reparent
(
parent:
parent
);
expect
(
child1
.
parent
,
equals
(
parent
));
expect
(
child2
.
parent
,
equals
(
parent
));
expect
(
parent
.
children
.
first
,
equals
(
child1
));
expect
(
parent
.
children
.
last
,
equals
(
child2
));
});
testWidgets
(
'Can remove children.'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusNode
parent
=
FocusNode
();
final
FocusAttachment
parentAttachment
=
parent
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
parentAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
child1Attachment
.
reparent
(
parent:
parent
);
child2Attachment
.
reparent
(
parent:
parent
);
expect
(
child1
.
parent
,
equals
(
parent
));
expect
(
child2
.
parent
,
equals
(
parent
));
expect
(
parent
.
children
.
first
,
equals
(
child1
));
expect
(
parent
.
children
.
last
,
equals
(
child2
));
child1Attachment
.
detach
();
expect
(
child1
.
parent
,
isNull
);
expect
(
child2
.
parent
,
equals
(
parent
));
expect
(
parent
.
children
.
first
,
equals
(
child2
));
expect
(
parent
.
children
.
last
,
equals
(
child2
));
child2Attachment
.
detach
();
expect
(
child1
.
parent
,
isNull
);
expect
(
child2
.
parent
,
isNull
);
expect
(
parent
.
children
,
isEmpty
);
});
testWidgets
(
'implements debugFillProperties'
,
(
WidgetTester
tester
)
async
{
final
DiagnosticPropertiesBuilder
builder
=
DiagnosticPropertiesBuilder
();
FocusNode
(
debugLabel:
'Label'
,
).
debugFillProperties
(
builder
);
final
List
<
String
>
description
=
builder
.
properties
.
where
((
DiagnosticsNode
n
)
=>
!
n
.
isFiltered
(
DiagnosticLevel
.
info
)).
map
((
DiagnosticsNode
n
)
=>
n
.
toString
()).
toList
();
expect
(
description
,
<
String
>[
'debugLabel: "Label"'
,
]);
});
});
group
(
FocusScopeNode
,
()
{
testWidgets
(
'Can setFirstFocus on a scope with no manager.'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope
=
FocusScopeNode
(
debugLabel:
'Scope'
);
scope
.
attach
(
context
);
final
FocusScopeNode
parent
=
FocusScopeNode
(
debugLabel:
'Parent'
);
parent
.
attach
(
context
);
final
FocusScopeNode
child1
=
FocusScopeNode
(
debugLabel:
'Child 1'
);
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusScopeNode
child2
=
FocusScopeNode
(
debugLabel:
'Child 2'
);
child2
.
attach
(
context
);
scope
.
setFirstFocus
(
parent
);
parent
.
setFirstFocus
(
child1
);
parent
.
setFirstFocus
(
child2
);
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasPrimaryFocus
,
isFalse
);
expect
(
scope
.
focusedChild
,
equals
(
parent
));
expect
(
parent
.
focusedChild
,
equals
(
child1
));
child1Attachment
.
detach
();
expect
(
scope
.
hasFocus
,
isFalse
);
expect
(
scope
.
focusedChild
,
equals
(
parent
));
});
testWidgets
(
'Removing a node removes it from scope.'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope
=
FocusScopeNode
();
final
FocusAttachment
scopeAttachment
=
scope
.
attach
(
context
);
final
FocusNode
parent
=
FocusNode
();
final
FocusAttachment
parentAttachment
=
parent
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
scopeAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parentAttachment
.
reparent
(
parent:
scope
);
child1Attachment
.
reparent
(
parent:
parent
);
child2Attachment
.
reparent
(
parent:
parent
);
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasPrimaryFocus
,
isTrue
);
expect
(
scope
.
focusedChild
,
equals
(
child1
));
child1Attachment
.
detach
();
expect
(
scope
.
hasFocus
,
isFalse
);
expect
(
scope
.
focusedChild
,
isNull
);
});
testWidgets
(
'Can add children to scope and focus'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope
=
FocusScopeNode
();
final
FocusAttachment
scopeAttachment
=
scope
.
attach
(
context
);
final
FocusNode
parent
=
FocusNode
();
final
FocusAttachment
parentAttachment
=
parent
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
scopeAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parentAttachment
.
reparent
(
parent:
scope
);
child1Attachment
.
reparent
(
parent:
parent
);
child2Attachment
.
reparent
(
parent:
parent
);
expect
(
scope
.
children
.
first
,
equals
(
parent
));
expect
(
parent
.
parent
,
equals
(
scope
));
expect
(
child1
.
parent
,
equals
(
parent
));
expect
(
child2
.
parent
,
equals
(
parent
));
expect
(
parent
.
children
.
first
,
equals
(
child1
));
expect
(
parent
.
children
.
last
,
equals
(
child2
));
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope
.
focusedChild
,
equals
(
child1
));
expect
(
parent
.
hasFocus
,
isTrue
);
expect
(
parent
.
hasPrimaryFocus
,
isFalse
);
expect
(
child1
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasPrimaryFocus
,
isTrue
);
expect
(
child2
.
hasFocus
,
isFalse
);
expect
(
child2
.
hasPrimaryFocus
,
isFalse
);
child2
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope
.
focusedChild
,
equals
(
child2
));
expect
(
parent
.
hasFocus
,
isTrue
);
expect
(
parent
.
hasPrimaryFocus
,
isFalse
);
expect
(
child1
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasPrimaryFocus
,
isFalse
);
expect
(
child2
.
hasFocus
,
isTrue
);
expect
(
child2
.
hasPrimaryFocus
,
isTrue
);
});
testWidgets
(
'Autofocus works.'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope
=
FocusScopeNode
(
debugLabel:
'Scope'
);
final
FocusAttachment
scopeAttachment
=
scope
.
attach
(
context
);
final
FocusNode
parent
=
FocusNode
(
debugLabel:
'Parent'
);
final
FocusAttachment
parentAttachment
=
parent
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
(
debugLabel:
'Child 1'
);
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
(
debugLabel:
'Child 2'
);
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
scopeAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parentAttachment
.
reparent
(
parent:
scope
);
child1Attachment
.
reparent
(
parent:
parent
);
child2Attachment
.
reparent
(
parent:
parent
);
scope
.
autofocus
(
child2
);
await
tester
.
pump
();
expect
(
scope
.
focusedChild
,
equals
(
child2
));
expect
(
parent
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasPrimaryFocus
,
isFalse
);
expect
(
child2
.
hasFocus
,
isTrue
);
expect
(
child2
.
hasPrimaryFocus
,
isTrue
);
child1
.
requestFocus
();
scope
.
autofocus
(
child2
);
await
tester
.
pump
();
expect
(
scope
.
focusedChild
,
equals
(
child1
));
expect
(
parent
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasPrimaryFocus
,
isTrue
);
expect
(
child2
.
hasFocus
,
isFalse
);
expect
(
child2
.
hasPrimaryFocus
,
isFalse
);
});
testWidgets
(
'Adding a focusedChild to a scope sets scope as focusedChild in parent scope'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope1
=
FocusScopeNode
();
final
FocusAttachment
scope1Attachment
=
scope1
.
attach
(
context
);
final
FocusScopeNode
scope2
=
FocusScopeNode
();
final
FocusAttachment
scope2Attachment
=
scope2
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
scope1Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
scope2Attachment
.
reparent
(
parent:
scope1
);
child1Attachment
.
reparent
(
parent:
scope1
);
child2Attachment
.
reparent
(
parent:
scope2
);
child2
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope2
.
focusedChild
,
equals
(
child2
));
expect
(
scope1
.
focusedChild
,
equals
(
scope2
));
expect
(
child1
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasPrimaryFocus
,
isFalse
);
expect
(
child2
.
hasFocus
,
isTrue
);
expect
(
child2
.
hasPrimaryFocus
,
isTrue
);
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope2
.
focusedChild
,
equals
(
child2
));
expect
(
scope1
.
focusedChild
,
equals
(
child1
));
expect
(
child1
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasPrimaryFocus
,
isTrue
);
expect
(
child2
.
hasFocus
,
isFalse
);
expect
(
child2
.
hasPrimaryFocus
,
isFalse
);
});
testWidgets
(
'Can move node with focus without losing focus'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope
=
FocusScopeNode
(
debugLabel:
'Scope'
);
final
FocusAttachment
scopeAttachment
=
scope
.
attach
(
context
);
final
FocusNode
parent1
=
FocusNode
(
debugLabel:
'Parent 1'
);
final
FocusAttachment
parent1Attachment
=
parent1
.
attach
(
context
);
final
FocusNode
parent2
=
FocusNode
(
debugLabel:
'Parent 2'
);
final
FocusAttachment
parent2Attachment
=
parent2
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
(
debugLabel:
'Child 1'
);
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
(
debugLabel:
'Child 2'
);
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
scopeAttachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parent1Attachment
.
reparent
(
parent:
scope
);
parent2Attachment
.
reparent
(
parent:
scope
);
child1Attachment
.
reparent
(
parent:
parent1
);
child2Attachment
.
reparent
(
parent:
parent1
);
expect
(
scope
.
children
.
first
,
equals
(
parent1
));
expect
(
scope
.
children
.
last
,
equals
(
parent2
));
expect
(
parent1
.
parent
,
equals
(
scope
));
expect
(
parent2
.
parent
,
equals
(
scope
));
expect
(
child1
.
parent
,
equals
(
parent1
));
expect
(
child2
.
parent
,
equals
(
parent1
));
expect
(
parent1
.
children
.
first
,
equals
(
child1
));
expect
(
parent1
.
children
.
last
,
equals
(
child2
));
child1
.
requestFocus
();
await
tester
.
pump
();
child1Attachment
.
reparent
(
parent:
parent2
);
await
tester
.
pump
();
expect
(
scope
.
focusedChild
,
equals
(
child1
));
expect
(
child1
.
parent
,
equals
(
parent2
));
expect
(
child2
.
parent
,
equals
(
parent1
));
expect
(
parent1
.
children
.
first
,
equals
(
child2
));
expect
(
parent2
.
children
.
first
,
equals
(
child1
));
});
testWidgets
(
'Can move node between scopes and lose scope focus'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope1
=
FocusScopeNode
()..
attach
(
context
);
final
FocusAttachment
scope1Attachment
=
scope1
.
attach
(
context
);
final
FocusScopeNode
scope2
=
FocusScopeNode
();
final
FocusAttachment
scope2Attachment
=
scope2
.
attach
(
context
);
final
FocusNode
parent1
=
FocusNode
();
final
FocusAttachment
parent1Attachment
=
parent1
.
attach
(
context
);
final
FocusNode
parent2
=
FocusNode
();
final
FocusAttachment
parent2Attachment
=
parent2
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
final
FocusNode
child3
=
FocusNode
();
final
FocusAttachment
child3Attachment
=
child3
.
attach
(
context
);
final
FocusNode
child4
=
FocusNode
();
final
FocusAttachment
child4Attachment
=
child4
.
attach
(
context
);
scope1Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
scope2Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parent1Attachment
.
reparent
(
parent:
scope1
);
parent2Attachment
.
reparent
(
parent:
scope2
);
child1Attachment
.
reparent
(
parent:
parent1
);
child2Attachment
.
reparent
(
parent:
parent1
);
child3Attachment
.
reparent
(
parent:
parent2
);
child4Attachment
.
reparent
(
parent:
parent2
);
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
scope1
.
focusedChild
,
equals
(
child1
));
expect
(
parent2
.
children
.
contains
(
child1
),
isFalse
);
child1Attachment
.
reparent
(
parent:
parent2
);
await
tester
.
pump
();
expect
(
scope1
.
focusedChild
,
isNull
);
expect
(
parent2
.
children
.
contains
(
child1
),
isTrue
);
});
testWidgets
(
'Can move focus between scopes and keep focus'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope1
=
FocusScopeNode
();
final
FocusAttachment
scope1Attachment
=
scope1
.
attach
(
context
);
final
FocusScopeNode
scope2
=
FocusScopeNode
();
final
FocusAttachment
scope2Attachment
=
scope2
.
attach
(
context
);
final
FocusNode
parent1
=
FocusNode
();
final
FocusAttachment
parent1Attachment
=
parent1
.
attach
(
context
);
final
FocusNode
parent2
=
FocusNode
();
final
FocusAttachment
parent2Attachment
=
parent2
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
();
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
final
FocusNode
child3
=
FocusNode
();
final
FocusAttachment
child3Attachment
=
child3
.
attach
(
context
);
final
FocusNode
child4
=
FocusNode
();
final
FocusAttachment
child4Attachment
=
child4
.
attach
(
context
);
scope1Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
scope2Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parent1Attachment
.
reparent
(
parent:
scope1
);
parent2Attachment
.
reparent
(
parent:
scope2
);
child1Attachment
.
reparent
(
parent:
parent1
);
child2Attachment
.
reparent
(
parent:
parent1
);
child3Attachment
.
reparent
(
parent:
parent2
);
child4Attachment
.
reparent
(
parent:
parent2
);
child4
.
requestFocus
();
await
tester
.
pump
();
child1
.
requestFocus
();
await
tester
.
pump
();
expect
(
child4
.
hasFocus
,
isFalse
);
expect
(
child4
.
hasPrimaryFocus
,
isFalse
);
expect
(
child1
.
hasFocus
,
isTrue
);
expect
(
child1
.
hasPrimaryFocus
,
isTrue
);
expect
(
scope1
.
hasFocus
,
isTrue
);
expect
(
scope1
.
hasPrimaryFocus
,
isFalse
);
expect
(
scope2
.
hasFocus
,
isFalse
);
expect
(
scope2
.
hasPrimaryFocus
,
isFalse
);
expect
(
parent1
.
hasFocus
,
isTrue
);
expect
(
parent2
.
hasFocus
,
isFalse
);
expect
(
scope1
.
focusedChild
,
equals
(
child1
));
expect
(
scope2
.
focusedChild
,
equals
(
child4
));
scope2
.
requestFocus
();
await
tester
.
pump
();
expect
(
child4
.
hasFocus
,
isTrue
);
expect
(
child4
.
hasPrimaryFocus
,
isTrue
);
expect
(
child1
.
hasFocus
,
isFalse
);
expect
(
child1
.
hasPrimaryFocus
,
isFalse
);
expect
(
scope1
.
hasFocus
,
isFalse
);
expect
(
scope1
.
hasPrimaryFocus
,
isFalse
);
expect
(
scope2
.
hasFocus
,
isTrue
);
expect
(
scope2
.
hasPrimaryFocus
,
isFalse
);
expect
(
parent1
.
hasFocus
,
isFalse
);
expect
(
parent2
.
hasFocus
,
isTrue
);
expect
(
scope1
.
focusedChild
,
equals
(
child1
));
expect
(
scope2
.
focusedChild
,
equals
(
child4
));
});
testWidgets
(
'Key handling bubbles up and terminates when handled.'
,
(
WidgetTester
tester
)
async
{
final
Set
<
FocusNode
>
receivedAnEvent
=
<
FocusNode
>{};
final
Set
<
FocusNode
>
shouldHandle
=
<
FocusNode
>{};
bool
handleEvent
(
FocusNode
node
,
RawKeyEvent
event
)
{
if
(
shouldHandle
.
contains
(
node
))
{
receivedAnEvent
.
add
(
node
);
return
true
;
}
return
false
;
}
void
sendEvent
()
{
receivedAnEvent
.
clear
();
sendFakeKeyEvent
(<
String
,
dynamic
>{
'type'
:
'keydown'
,
'keymap'
:
'fuchsia'
,
'hidUsage'
:
0x04
,
'codePoint'
:
0x64
,
'modifiers'
:
RawKeyEventDataFuchsia
.
modifierLeftMeta
,
});
}
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope1
=
FocusScopeNode
(
debugLabel:
'Scope 1'
);
final
FocusAttachment
scope1Attachment
=
scope1
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusScopeNode
scope2
=
FocusScopeNode
(
debugLabel:
'Scope 2'
);
final
FocusAttachment
scope2Attachment
=
scope2
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
parent1
=
FocusNode
(
debugLabel:
'Parent 1'
);
final
FocusAttachment
parent1Attachment
=
parent1
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
parent2
=
FocusNode
(
debugLabel:
'Parent 2'
);
final
FocusAttachment
parent2Attachment
=
parent2
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
child1
=
FocusNode
(
debugLabel:
'Child 1'
);
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
child2
=
FocusNode
(
debugLabel:
'Child 2'
);
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
child3
=
FocusNode
(
debugLabel:
'Child 3'
);
final
FocusAttachment
child3Attachment
=
child3
.
attach
(
context
,
onKey:
handleEvent
);
final
FocusNode
child4
=
FocusNode
(
debugLabel:
'Child 4'
);
final
FocusAttachment
child4Attachment
=
child4
.
attach
(
context
,
onKey:
handleEvent
);
scope1Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
scope2Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parent1Attachment
.
reparent
(
parent:
scope1
);
parent2Attachment
.
reparent
(
parent:
scope2
);
child1Attachment
.
reparent
(
parent:
parent1
);
child2Attachment
.
reparent
(
parent:
parent1
);
child3Attachment
.
reparent
(
parent:
parent2
);
child4Attachment
.
reparent
(
parent:
parent2
);
child4
.
requestFocus
();
await
tester
.
pump
();
shouldHandle
.
addAll
(<
FocusNode
>{
scope2
,
parent2
,
child2
,
child4
});
sendEvent
();
expect
(
receivedAnEvent
,
equals
(<
FocusNode
>{
child4
}));
shouldHandle
.
remove
(
child4
);
sendEvent
();
expect
(
receivedAnEvent
,
equals
(<
FocusNode
>{
parent2
}));
shouldHandle
.
remove
(
parent2
);
sendEvent
();
expect
(
receivedAnEvent
,
equals
(<
FocusNode
>{
scope2
}));
shouldHandle
.
clear
();
sendEvent
();
expect
(
receivedAnEvent
,
isEmpty
);
child1
.
requestFocus
();
await
tester
.
pump
();
shouldHandle
.
addAll
(<
FocusNode
>{
scope2
,
parent2
,
child2
,
child4
});
sendEvent
();
// Since none of the focused nodes handle this event, nothing should
// receive it.
expect
(
receivedAnEvent
,
isEmpty
);
});
testWidgets
(
'implements debugFillProperties'
,
(
WidgetTester
tester
)
async
{
final
DiagnosticPropertiesBuilder
builder
=
DiagnosticPropertiesBuilder
();
FocusScopeNode
(
debugLabel:
'Scope Label'
,
).
debugFillProperties
(
builder
);
final
List
<
String
>
description
=
builder
.
properties
.
where
((
DiagnosticsNode
n
)
=>
!
n
.
isFiltered
(
DiagnosticLevel
.
info
)).
map
((
DiagnosticsNode
n
)
=>
n
.
toString
()).
toList
();
expect
(
description
,
<
String
>[
'debugLabel: "Scope Label"'
,
]);
});
testWidgets
(
'debugDescribeFocusTree produces correct output'
,
(
WidgetTester
tester
)
async
{
final
BuildContext
context
=
await
setupWidget
(
tester
);
final
FocusScopeNode
scope1
=
FocusScopeNode
(
debugLabel:
'Scope 1'
);
final
FocusAttachment
scope1Attachment
=
scope1
.
attach
(
context
);
final
FocusScopeNode
scope2
=
FocusScopeNode
();
// No label, Just to test that it works.
final
FocusAttachment
scope2Attachment
=
scope2
.
attach
(
context
);
final
FocusNode
parent1
=
FocusNode
(
debugLabel:
'Parent 1'
);
final
FocusAttachment
parent1Attachment
=
parent1
.
attach
(
context
);
final
FocusNode
parent2
=
FocusNode
(
debugLabel:
'Parent 2'
);
final
FocusAttachment
parent2Attachment
=
parent2
.
attach
(
context
);
final
FocusNode
child1
=
FocusNode
(
debugLabel:
'Child 1'
);
final
FocusAttachment
child1Attachment
=
child1
.
attach
(
context
);
final
FocusNode
child2
=
FocusNode
();
// No label, Just to test that it works.
final
FocusAttachment
child2Attachment
=
child2
.
attach
(
context
);
final
FocusNode
child3
=
FocusNode
(
debugLabel:
'Child 3'
);
final
FocusAttachment
child3Attachment
=
child3
.
attach
(
context
);
final
FocusNode
child4
=
FocusNode
(
debugLabel:
'Child 4'
);
final
FocusAttachment
child4Attachment
=
child4
.
attach
(
context
);
scope1Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
scope2Attachment
.
reparent
(
parent:
tester
.
binding
.
focusManager
.
rootScope
);
parent1Attachment
.
reparent
(
parent:
scope1
);
parent2Attachment
.
reparent
(
parent:
scope2
);
child1Attachment
.
reparent
(
parent:
parent1
);
child2Attachment
.
reparent
(
parent:
parent1
);
child3Attachment
.
reparent
(
parent:
parent2
);
child4Attachment
.
reparent
(
parent:
parent2
);
child4
.
requestFocus
();
await
tester
.
pump
();
final
String
description
=
debugDescribeFocusTree
();
expect
(
description
,
equalsIgnoringHashCodes
(
'FocusManager#00000
\n
'
' │ currentFocus: FocusNode#00000
\n
'
' │
\n
'
' └─rootScope: FocusScopeNode#00000
\n
'
' │ FOCUSED
\n
'
' │ debugLabel: "Root Focus Scope"
\n
'
' │ focusedChild: FocusScopeNode#00000
\n
'
' │
\n
'
' ├─Child 1: FocusScopeNode#00000
\n
'
' │ │ context: Container-[GlobalKey#00000]
\n
'
' │ │ debugLabel: "Scope 1"
\n
'
' │ │
\n
'
' │ └─Child 1: FocusNode#00000
\n
'
' │ │ context: Container-[GlobalKey#00000]
\n
'
' │ │ debugLabel: "Parent 1"
\n
'
' │ │
\n
'
' │ ├─Child 1: FocusNode#00000
\n
'
' │ │ context: Container-[GlobalKey#00000]
\n
'
' │ │ debugLabel: "Child 1"
\n
'
' │ │
\n
'
' │ └─Child 2: FocusNode#00000
\n
'
' │ context: Container-[GlobalKey#00000]
\n
'
' │
\n
'
' └─Child 2: FocusScopeNode#00000
\n
'
' │ context: Container-[GlobalKey#00000]
\n
'
' │ FOCUSED
\n
'
' │ focusedChild: FocusNode#00000
\n
'
' │
\n
'
' └─Child 1: FocusNode#00000
\n
'
' │ context: Container-[GlobalKey#00000]
\n
'
' │ FOCUSED
\n
'
' │ debugLabel: "Parent 2"
\n
'
' │
\n
'
' ├─Child 1: FocusNode#00000
\n
'
' │ context: Container-[GlobalKey#00000]
\n
'
' │ debugLabel: "Child 3"
\n
'
' │
\n
'
' └─Child 2: FocusNode#00000
\n
'
' context: Container-[GlobalKey#00000]
\n
'
' FOCUSED
\n
'
' debugLabel: "Child 4"
\n
'
));
});
});
}
packages/flutter/test/widgets/focus_scope_test.dart
View file @
590cc27b
...
...
@@ -5,25 +5,22 @@
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/widgets.dart'
;
class
TestFocus
extends
StatefulWidget
{
const
TestFocus
({
class
TestFocus
able
extends
StatefulWidget
{
const
TestFocus
able
({
Key
key
,
this
.
debugLabel
,
this
.
name
=
'a'
,
this
.
autofocus
=
false
,
})
:
super
(
key:
key
);
final
String
debugLabel
;
final
String
name
;
final
bool
autofocus
;
@override
TestFocus
State
createState
()
=>
TestFocus
State
();
TestFocus
ableState
createState
()
=>
TestFocusable
State
();
}
class
TestFocusState
extends
State
<
TestFocus
>
{
FocusNode
focusNode
=
FocusNode
();
FocusAttachment
focusAttachment
;
class
TestFocusableState
extends
State
<
TestFocusable
>
{
final
FocusNode
focusNode
=
FocusNode
();
bool
_didAutofocus
=
false
;
@override
...
...
@@ -32,16 +29,9 @@ class TestFocusState extends State<TestFocus> {
super
.
dispose
();
}
@override
void
initState
()
{
super
.
initState
();
focusNode
=
FocusNode
(
debugLabel:
widget
.
debugLabel
);
focusAttachment
=
focusNode
.
attach
(
context
);
}
@override
Widget
build
(
BuildContext
context
)
{
focusAttachment
.
reparent
(
);
FocusScope
.
of
(
context
).
reparentIfNeeded
(
focusNode
);
if
(!
_didAutofocus
&&
widget
.
autofocus
)
{
_didAutofocus
=
true
;
FocusScope
.
of
(
context
).
autofocus
(
focusNode
);
...
...
@@ -64,899 +54,676 @@ class TestFocusState extends State<TestFocus> {
}
void
main
(
)
{
group
(
FocusScope
,
()
{
testWidgets
(
'Can focus'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
key
=
GlobalKey
();
testWidgets
(
'Can focus'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
key
=
GlobalKey
();
await
tester
.
pumpWidget
(
TestFocus
(
key:
key
,
name:
'a'
),
);
await
tester
.
pumpWidget
(
TestFocusable
(
key:
key
,
name:
'a'
),
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
FocusScope
.
of
(
key
.
currentContext
).
requestFocus
(
key
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
FocusScope
.
of
(
key
.
currentContext
).
requestFocus
(
key
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
});
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
});
testWidgets
(
'Can unfocus'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocus
State
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocus
State
>
keyB
=
GlobalKey
();
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyA
,
name:
'a'
),
TestFocus
(
key:
keyB
,
name:
'b'
),
],
),
);
testWidgets
(
'Can unfocus'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusable
State
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusable
State
>
keyB
=
GlobalKey
();
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
),
TestFocusable
(
key:
keyB
,
name:
'b'
),
],
),
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
// Set focus to the "B" node to unfocus the "A" node.
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
// Set focus to the "B" node to unfocus the "A" node.
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'B FOCUSED'
),
findsOneWidget
);
});
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'B FOCUSED'
),
findsOneWidget
);
});
testWidgets
(
'Can have multiple focused children and they update accordingly'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocus
State
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocus
State
>
keyB
=
GlobalKey
();
testWidgets
(
'Can have multiple focused children and they update accordingly'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusable
State
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusable
State
>
keyB
=
GlobalKey
();
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyA
,
name:
'a'
,
autofocus:
true
,
),
TestFocus
(
key:
keyB
,
name:
'b'
,
),
],
),
);
// Autofocus is delayed one frame.
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'A FOCUSED'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'b'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'B FOCUSED'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'a'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
// This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again.
testWidgets
(
'Can move focus in and out of FocusScope'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
(
debugLabel:
'Parent Scope Node'
);
final
FocusScopeNode
childFocusScope
=
FocusScopeNode
(
debugLabel:
'Child Scope Node'
);
final
GlobalKey
<
TestFocusState
>
key
=
GlobalKey
();
// Initially create the focus inside of the parent FocusScope.
await
tester
.
pumpWidget
(
FocusScope
(
debugLabel:
'Parent Scope'
,
node:
parentFocusScope
,
autofocus:
true
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
key
,
name:
'a'
,
debugLabel:
'Child'
,
),
],
),
),
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
FocusScope
.
of
(
key
.
currentContext
).
requestFocus
(
key
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
parentFocusScope
,
hasAGoodToStringDeep
);
expect
(
parentFocusScope
.
toStringDeep
(),
equalsIgnoringHashCodes
(
'FocusScopeNode#00000
\n
'
' │ context: FocusScope
\n
'
' │ FOCUSED
\n
'
' │ debugLabel: "Parent Scope Node"
\n
'
' │ focusedChild: FocusNode#00000
\n
'
' │
\n
'
' └─Child 1: FocusNode#00000
\n
'
' context: TestFocus-[LabeledGlobalKey<TestFocusState>#00000]
\n
'
' FOCUSED
\n
'
' debugLabel: "Child"
\n
'
),
);
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
,
hasAGoodToStringDeep
);
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
toStringDeep
(
minLevel:
DiagnosticLevel
.
info
),
equalsIgnoringHashCodes
(
'FocusScopeNode#00000
\n
'
' │ FOCUSED
\n
'
' │ debugLabel: "Root Focus Scope"
\n
'
' │ focusedChild: FocusScopeNode#00000
\n
'
' │
\n
'
' └─Child 1: FocusScopeNode#00000
\n
'
' │ context: FocusScope
\n
'
' │ FOCUSED
\n
'
' │ debugLabel: "Parent Scope Node"
\n
'
' │ focusedChild: FocusNode#00000
\n
'
' │
\n
'
' └─Child 1: FocusNode#00000
\n
'
' context: TestFocus-[LabeledGlobalKey<TestFocusState>#00000]
\n
'
' FOCUSED
\n
'
' debugLabel: "Child"
\n
'
),
);
// Add the child focus scope to the focus tree.
final
FocusAttachment
childAttachment
=
childFocusScope
.
attach
(
key
.
currentContext
);
parentFocusScope
.
setFirstFocus
(
childFocusScope
);
await
tester
.
pumpAndSettle
();
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
// Now add the child focus scope with no child focusable in it to the tree.
await
tester
.
pumpWidget
(
FocusScope
(
debugLabel:
'Parent Scope'
,
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
key
,
debugLabel:
'Child'
,
),
FocusScope
(
debugLabel:
'Child Scope'
,
node:
childFocusScope
,
child:
Container
(),
),
],
),
),
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
// Now move the existing focus node into the child focus scope.
await
tester
.
pumpWidget
(
FocusScope
(
debugLabel:
'Parent Scope'
,
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
FocusScope
(
debugLabel:
'Child Scope'
,
node:
childFocusScope
,
child:
TestFocus
(
key:
key
,
debugLabel:
'Child'
,
),
),
],
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
autofocus:
true
,
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
// Now remove the child focus scope.
await
tester
.
pumpWidget
(
FocusScope
(
debugLabel:
'Parent Scope'
,
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
key
,
debugLabel:
'Child'
,
),
],
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
);
// Autofocus is delayed one frame.
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'A FOCUSED'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'b'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'B FOCUSED'
),
findsOneWidget
);
await
tester
.
tap
(
find
.
text
(
'a'
));
await
tester
.
pump
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
// This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again.
testWidgets
(
'Can move focus in and out of FocusScope'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
();
final
FocusScopeNode
childFocusScope
=
FocusScopeNode
();
final
GlobalKey
<
TestFocusableState
>
key
=
GlobalKey
();
// Initially create the focus inside of the parent FocusScope.
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
autofocus:
true
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
key
,
name:
'a'
),
],
),
);
),
);
await
tester
.
pumpAndSettle
();
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
FocusScope
.
of
(
key
.
currentContext
).
requestFocus
(
key
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
// Must detach the child because we had to attach it in order to call
// setFirstFocus before adding to the widget.
childAttachment
.
detach
();
});
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing focused widget doesn't move focus to next widget"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
();
expect
(
parentFocusScope
,
hasAGoodToStringDeep
);
expect
(
parentFocusScope
.
toStringDeep
(
minLevel:
DiagnosticLevel
.
info
),
equalsIgnoringHashCodes
(
'FocusScopeNode#00000
\n
'
' focus: FocusNode#00000(FOCUSED)
\n
'
),
);
await
tester
.
pumpWidget
(
Column
(
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
,
hasAGoodToStringDeep
);
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
toStringDeep
(
minLevel:
DiagnosticLevel
.
info
),
equalsIgnoringHashCodes
(
'FocusScopeNode#00000
\n
'
' └─child 1: FocusScopeNode#00000
\n
'
' focus: FocusNode#00000(FOCUSED)
\n
'
),
);
// Add the child focus scope to the focus tree.
parentFocusScope
.
setFirstFocus
(
childFocusScope
);
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
// Now add the child focus scope with no focus node in it to the tree.
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyA
,
name:
'a'
,
),
TestFocus
(
key:
keyB
,
name:
'b'
,
TestFocusable
(
key:
key
),
FocusScope
(
node:
childFocusScope
,
child:
Container
(),
),
],
),
);
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
await
tester
.
pumpAndSettle
();
// Now move the existing focus node into the child focus scope.
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
FocusScope
(
node:
childFocusScope
,
child:
TestFocusable
(
key:
key
),
),
],
),
),
);
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
Column
(
// Now remove the child focus scope.
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyB
,
name:
'b'
,
),
TestFocusable
(
key:
key
),
],
),
);
),
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFals
e
);
expect
(
find
.
text
(
'b
'
),
findsOneWidget
);
});
expect
(
key
.
currentState
.
focusNode
.
hasFocus
,
isTru
e
);
expect
(
find
.
text
(
'A FOCUSED
'
),
findsOneWidget
);
});
testWidgets
(
'Adding a new FocusScope attaches the child it to its parent.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
(
debugLabel:
'Parent Scope Node'
);
final
FocusScopeNode
childFocusScope
=
FocusScopeNode
(
debugLabel:
'Child Scope Node'
);
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing focused widget doesn't move focus to next widget"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
(
);
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
(
);
await
tester
.
pumpWidget
(
FocusScope
(
node:
childFocusScope
,
child:
TestFocus
(
debugLabel:
'Child'
,
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
),
);
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
expect
(
FocusScope
.
of
(
keyA
.
currentContext
),
equals
(
childFocusScope
));
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
FocusScope
(
node:
childFocusScope
,
child:
TestFocus
(
debugLabel:
'Child'
,
key:
keyA
,
),
),
),
);
await
tester
.
pump
();
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
// Node keeps it's focus when moved to the new scope.
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
});
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing focused widget doesn't move focus to next widget within FocusScope"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
(
debugLabel:
'Parent Scope'
);
await
tester
.
pumpWidget
(
FocusScope
(
debugLabel:
'Parent Scope'
,
node:
parentFocusScope
,
autofocus:
true
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Widget A'
,
key:
keyA
,
name:
'a'
,
),
TestFocus
(
debugLabel:
'Widget B'
,
key:
keyB
,
name:
'b'
,
),
],
),
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
final
FocusScopeNode
scope
=
FocusScope
.
of
(
keyA
.
currentContext
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
scope
);
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyB
,
name:
'b'
,
),
],
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
),
);
],
),
);
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
testWidgets
(
'Removing a FocusScope removes its node from the tree'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
scopeKeyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
scopeKeyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
(
debugLabel:
'Parent Scope'
);
testWidgets
(
'Adding a new FocusScope attaches the child it to its parent.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
();
final
FocusScopeNode
childFocusScope
=
FocusScopeNode
();
// This checks both FocusScopes that have their own nodes, as well as those
// that use external nodes.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
key:
scopeKeyA
,
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child A'
,
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
key:
scopeKeyB
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child B'
,
key:
keyB
,
name:
'b'
,
),
],
),
),
],
await
tester
.
pumpWidget
(
FocusScope
(
node:
childFocusScope
,
child:
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
);
),
);
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
final
FocusScopeNode
bScope
=
FocusScope
.
of
(
keyB
.
currentContext
);
final
FocusScopeNode
aScope
=
FocusScope
.
of
(
keyA
.
currentContext
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
bScope
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
aScope
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
FocusScope
.
of
(
keyA
.
currentContext
).
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
await
tester
.
pumpWidget
(
Container
());
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
FocusScope
(
node:
childFocusScope
,
child:
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
),
),
);
await
tester
.
pump
();
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
children
,
isEmpty
);
});
expect
(
childFocusScope
.
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
});
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
(
debugLabel:
'Parent Scope 1'
);
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
(
debugLabel:
'Parent Scope 2'
);
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing focused widget doesn't move focus to next widget within FocusScope"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope
=
FocusScopeNode
();
await
tester
.
pumpWidget
(
Column
(
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
autofocus:
true
,
child:
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child A'
,
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child B'
,
key:
keyB
,
name:
'b'
,
),
],
),
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
],
),
);
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
final
FocusScopeNode
aScope
=
FocusScope
.
of
(
keyA
.
currentContext
);
final
FocusScopeNode
bScope
=
FocusScope
.
of
(
keyB
.
currentContext
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
bScope
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
aScope
);
await
tester
.
pumpAndSettle
();
expect
(
FocusScope
.
of
(
keyA
.
currentContext
).
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
// If the FocusScope widgets are not pinned with GlobalKeys, then the first
// one remains and gets its guts replaced with the parentFocusScope2 and the
// "B" test widget, and in the process, the focus manager loses track of the
// focus.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyB
,
name:
'b'
,
autofocus:
true
,
),
],
),
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
)
;
await
tester
.
pump
(
);
)
,
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
testWidgets
(
'Moving widget from one scope to another retains focus'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
();
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
();
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
();
await
tester
.
pumpAndSettle
();
await
tester
.
pumpWidget
(
Column
(
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
FocusScope
(
node:
parentFocusScope
,
child:
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyB
,
name:
'b'
,
),
],
),
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
);
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
final
FocusScopeNode
aScope
=
FocusScope
.
of
(
keyA
.
currentContext
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
aScope
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
// By "pinned", it means kept in the tree by a GlobalKey.
testWidgets
(
'Removing pinned focused scope moves focus to focused widget within next FocusScope'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
scopeKeyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
scopeKeyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
();
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
();
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyB
,
name:
'b'
,
)
,
]
,
)
,
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
key:
scopeKeyA
,
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
)
,
]
,
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
key:
scopeKeyB
,
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
],
),
);
),
],
),
);
await
tester
.
pump
();
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyB
.
currentContext
));
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
await
tester
.
pumpAndSettle
();
testWidgets
(
'Moving FocusScopeNodes retains focus'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
(
debugLabel:
'Scope 1'
);
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
(
debugLabel:
'Scope 2'
);
final
GlobalKey
<
TestFocusState
>
keyA
=
GlobalKey
(
);
final
GlobalKey
<
TestFocusState
>
keyB
=
GlobalKey
(
);
expect
(
FocusScope
.
of
(
keyA
.
currentContext
).
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child A'
,
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child B'
,
key:
keyB
,
name:
'b'
,
),
],
),
// Since the FocusScope widgets are pinned with GlobalKeys, when the first
// one gets removed, the second one stays registered with the focus
// manager and ends up getting the focus since it remains as part of the
// focus tree.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
key:
scopeKeyB
,
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
autofocus:
true
,
),
],
),
],
),
);
),
],
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
final
FocusScopeNode
aScope
=
FocusScope
.
of
(
keyA
.
currentContext
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
aScope
);
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'B FOCUSED'
),
findsOneWidget
);
});
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
"Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope"
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
();
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
();
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
();
// This just swaps the FocusScopeNodes that the FocusScopes have in them.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child A'
,
key:
keyA
,
name:
'a'
,
),
],
),
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
],
),
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocus
(
debugLabel:
'Child B'
,
key:
keyB
,
name:
'b'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
],
),
);
),
],
),
);
FocusScope
.
of
(
keyB
.
currentContext
).
requestFocus
(
keyB
.
currentState
.
focusNode
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyB
.
currentContext
));
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
expect
(
FocusScope
.
of
(
keyA
.
currentContext
).
isFirstFocus
,
isTrue
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
// If the FocusScope widgets are not pinned with GlobalKeys, then the first
// one remains and gets its guts replaced with the parentFocusScope2 and the
// "B" test widget, and in the process, the focus manager loses track of the
// focus.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
autofocus:
true
,
),
],
),
),
],
),
);
await
tester
.
pump
();
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
group
(
Focus
,
()
{
testWidgets
(
'Focus.of stops at the nearest FocusScope.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key1
=
GlobalKey
(
debugLabel:
'1'
);
final
GlobalKey
key2
=
GlobalKey
(
debugLabel:
'2'
);
final
GlobalKey
key3
=
GlobalKey
(
debugLabel:
'3'
);
final
GlobalKey
key4
=
GlobalKey
(
debugLabel:
'4'
);
final
GlobalKey
key5
=
GlobalKey
(
debugLabel:
'5'
);
final
GlobalKey
key6
=
GlobalKey
(
debugLabel:
'6'
);
await
tester
.
pumpWidget
(
Focus
(
key:
key1
,
debugLabel:
'Key 1'
,
child:
Container
(
key:
key2
,
child:
Focus
(
debugLabel:
'Key 3'
,
key:
key3
,
child:
Container
(
key:
key4
,
child:
Focus
(
debugLabel:
'Key 5'
,
key:
key5
,
child:
Container
(
key:
key6
,
),
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
'Moving widget from one scope to another does not retain focus'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
();
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
();
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
();
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
)
,
]
,
),
),
),
);
final
Element
element1
=
tester
.
element
(
find
.
byKey
(
key1
));
final
Element
element2
=
tester
.
element
(
find
.
byKey
(
key2
));
final
Element
element3
=
tester
.
element
(
find
.
byKey
(
key3
));
final
Element
element4
=
tester
.
element
(
find
.
byKey
(
key4
));
final
Element
element5
=
tester
.
element
(
find
.
byKey
(
key5
));
final
Element
element6
=
tester
.
element
(
find
.
byKey
(
key6
));
final
FocusNode
root
=
element1
.
owner
.
focusManager
.
rootScope
;
expect
(
Focus
.
of
(
element1
),
equals
(
root
));
expect
(
Focus
.
of
(
element2
).
parent
,
equals
(
root
));
expect
(
Focus
.
of
(
element3
).
parent
,
equals
(
root
));
expect
(
Focus
.
of
(
element4
).
parent
.
parent
,
equals
(
root
));
expect
(
Focus
.
of
(
element5
).
parent
.
parent
,
equals
(
root
));
expect
(
Focus
.
of
(
element6
).
parent
.
parent
.
parent
,
equals
(
root
));
});
testWidgets
(
'Can traverse Focus children.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key1
=
GlobalKey
(
debugLabel:
'1'
);
final
GlobalKey
key2
=
GlobalKey
(
debugLabel:
'2'
);
final
GlobalKey
key3
=
GlobalKey
(
debugLabel:
'3'
);
final
GlobalKey
key4
=
GlobalKey
(
debugLabel:
'4'
);
final
GlobalKey
key5
=
GlobalKey
(
debugLabel:
'5'
);
final
GlobalKey
key6
=
GlobalKey
(
debugLabel:
'6'
);
final
GlobalKey
key7
=
GlobalKey
(
debugLabel:
'7'
);
final
GlobalKey
key8
=
GlobalKey
(
debugLabel:
'8'
);
await
tester
.
pumpWidget
(
Focus
(
child:
Column
(
key:
key1
,
children:
<
Widget
>[
Focus
(
key:
key2
,
child:
Container
(
child:
Focus
(
key:
key3
,
child:
Container
(),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
),
Focus
(
key:
key4
,
child:
Container
(
child:
Focus
(
key:
key5
,
child:
Container
(),
),
],
),
),
],
),
);
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
),
Focus
(
key:
key6
,
child:
Column
(
children:
<
Widget
>[
Focus
(
key:
key7
,
child:
Container
(),
),
Focus
(
key:
key8
,
child:
Container
(),
),
],
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
)
,
]
,
]
,
)
,
),
),
);
final
Element
firstScope
=
tester
.
element
(
find
.
byKey
(
key1
));
final
List
<
FocusNode
>
nodes
=
<
FocusNode
>[];
final
List
<
Key
>
keys
=
<
Key
>[];
bool
visitor
(
FocusNode
node
)
{
nodes
.
add
(
node
);
keys
.
add
(
node
.
context
.
widget
.
key
);
return
true
;
}
await
tester
.
pump
();
Focus
.
of
(
firstScope
).
descendants
.
forEach
(
visitor
);
expect
(
nodes
.
length
,
equals
(
7
));
expect
(
keys
.
length
,
equals
(
7
));
// Depth first.
expect
(
keys
,
equals
(<
Key
>[
key3
,
key2
,
key5
,
key4
,
key7
,
key8
,
key6
]));
// Just traverses a sub-tree.
final
Element
secondScope
=
tester
.
element
(
find
.
byKey
(
key7
));
nodes
.
clear
();
keys
.
clear
();
Focus
.
of
(
secondScope
).
descendants
.
forEach
(
visitor
);
expect
(
nodes
.
length
,
equals
(
2
));
expect
(
keys
,
equals
(<
Key
>[
key7
,
key8
]));
});
testWidgets
(
'Can set focus.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key1
=
GlobalKey
(
debugLabel:
'1'
);
bool
gotFocus
;
await
tester
.
pumpWidget
(
Focus
(
onFocusChange:
(
bool
focused
)
=>
gotFocus
=
focused
,
child:
Container
(
key:
key1
),
),
);
final
Element
firstNode
=
tester
.
element
(
find
.
byKey
(
key1
));
final
FocusNode
node
=
Focus
.
of
(
firstNode
);
node
.
requestFocus
();
await
tester
.
pump
();
expect
(
gotFocus
,
isTrue
);
expect
(
node
.
hasFocus
,
isTrue
);
});
testWidgets
(
'Can focus root node.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key1
=
GlobalKey
(
debugLabel:
'1'
);
await
tester
.
pumpWidget
(
Focus
(
key:
key1
,
child:
Container
(),
),
);
final
Element
firstElement
=
tester
.
element
(
find
.
byKey
(
key1
));
final
FocusNode
rootNode
=
Focus
.
of
(
firstElement
);
rootNode
.
requestFocus
();
],
),
);
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
rootNode
.
hasFocus
,
isTrue
);
expect
(
rootNode
,
equals
(
firstElement
.
owner
.
focusManager
.
rootScope
));
});
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
testWidgets
(
'Nodes are removed when all Focuses are removed.'
,
(
WidgetTester
tester
)
async
{
final
GlobalKey
key1
=
GlobalKey
(
debugLabel:
'1'
);
bool
gotFocus
;
// Arguably, this isn't correct behavior, but it is what happens now.
testWidgets
(
'Moving FocusScopeNodes does not retain focus'
,
(
WidgetTester
tester
)
async
{
final
FocusScopeNode
parentFocusScope1
=
FocusScopeNode
();
final
FocusScopeNode
parentFocusScope2
=
FocusScopeNode
();
final
GlobalKey
<
TestFocusableState
>
keyA
=
GlobalKey
();
final
GlobalKey
<
TestFocusableState
>
keyB
=
GlobalKey
();
await
tester
.
pumpWidget
(
FocusScope
(
child:
Focus
(
onFocusChange:
(
bool
focused
)
=>
gotFocus
=
focused
,
child:
Container
(
key:
key1
),
),
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
),
],
),
);
final
Element
firstNode
=
tester
.
element
(
find
.
byKey
(
key1
));
final
FocusNode
node
=
Focus
.
of
(
firstNode
);
node
.
requestFocus
();
FocusScope
.
of
(
keyA
.
currentContext
).
requestFocus
(
keyA
.
currentState
.
focusNode
);
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
setFirstFocus
(
FocusScope
.
of
(
keyA
.
currentContext
));
await
tester
.
pump
();
await
tester
.
pumpAndSettle
();
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isTrue
);
expect
(
find
.
text
(
'A FOCUSED'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
expect
(
gotFocus
,
isTrue
);
expect
(
node
.
hasFocus
,
isTrue
);
// This just swaps the FocusScopeNodes that the FocusScopes have in them.
await
tester
.
pumpWidget
(
Column
(
children:
<
Widget
>[
FocusScope
(
node:
parentFocusScope2
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyA
,
name:
'a'
,
),
],
),
),
FocusScope
(
node:
parentFocusScope1
,
child:
Column
(
children:
<
Widget
>[
TestFocusable
(
key:
keyB
,
name:
'b'
,
),
],
),
),
],
),
);
await
tester
.
pump
Widget
(
Container
()
);
await
tester
.
pump
(
);
expect
(
WidgetsBinding
.
instance
.
focusManager
.
rootScope
.
descendants
,
isEmpty
);
expect
(
keyA
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'a'
),
findsOneWidget
);
expect
(
keyB
.
currentState
.
focusNode
.
hasFocus
,
isFalse
);
expect
(
find
.
text
(
'b'
),
findsOneWidget
);
});
}
packages/flutter/test/widgets/raw_keyboard_listener_test.dart
View file @
590cc27b
...
...
@@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages
.
handlePlatformMessage
(
SystemChannels
.
keyEvent
.
name
,
SystemChannels
.
keyEvent
.
codec
.
encodeMessage
(
data
),
(
ByteData
data
)
{},
(
ByteData
data
)
{
},
);
}
...
...
@@ -29,15 +29,13 @@ void main() {
final
FocusNode
focusNode
=
FocusNode
();
await
tester
.
pumpWidget
(
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
events
.
add
,
child:
Container
(),
),
);
await
tester
.
pumpWidget
(
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
events
.
add
,
child:
Container
(),
));
focusNode
.
requestFocus
(
);
tester
.
binding
.
focusManager
.
rootScope
.
requestFocus
(
focusNode
);
await
tester
.
idle
();
sendFakeKeyEvent
(<
String
,
dynamic
>{
...
...
@@ -67,15 +65,13 @@ void main() {
final
FocusNode
focusNode
=
FocusNode
();
await
tester
.
pumpWidget
(
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
events
.
add
,
child:
Container
(),
),
);
await
tester
.
pumpWidget
(
RawKeyboardListener
(
focusNode:
focusNode
,
onKey:
events
.
add
,
child:
Container
(),
));
focusNode
.
requestFocus
(
);
tester
.
binding
.
focusManager
.
rootScope
.
requestFocus
(
focusNode
);
await
tester
.
idle
();
sendFakeKeyEvent
(<
String
,
dynamic
>{
...
...
packages/flutter_test/lib/src/binding.dart
View file @
590cc27b
...
...
@@ -692,7 +692,6 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError
.
onError
=
_oldExceptionHandler
;
_pendingExceptionDetails
=
null
;
_parentZone
=
null
;
buildOwner
.
focusManager
=
FocusManager
();
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment