Unverified Commit 7775c237 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

[Re-Land] Implement focus traversal for desktop platforms. (#31614)

This re-lands the Focus changes in #30040. Correctness changes in routes.dart, and removes the automatic requesting of focus on reparent when there is no current focus, which caused undesirable selections.

Addresses #11344, #1608, #13264, and #1678
Fixes #30084
Fixes #26704
parent fdae7bb8
...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> { ...@@ -62,7 +62,7 @@ class _HardwareKeyDemoState extends State<RawKeyboardDemo> {
if (!_focusNode.hasFocus) { if (!_focusNode.hasFocus) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
FocusScope.of(context).requestFocus(_focusNode); _focusNode.requestFocus();
}, },
child: Text('Tap to focus', style: textTheme.display1), child: Text('Tap to focus', style: textTheme.display1),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatefulWidget(), body: MyStatefulWidget(),
), ),
); );
......
...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget { ...@@ -17,7 +17,7 @@ class MyApp extends StatelessWidget {
return MaterialApp( return MaterialApp(
title: _title, title: _title,
home: Scaffold( home: Scaffold(
appBar: AppBar(title: Text(_title)), appBar: AppBar(title: const Text(_title)),
body: MyStatelessWidget(), body: MyStatelessWidget(),
), ),
); );
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -304,7 +304,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
tabs = List<Widget>(widget.tabNumber); tabs = List<Widget>(widget.tabNumber);
tabFocusNodes = List<FocusScopeNode>.generate( tabFocusNodes = List<FocusScopeNode>.generate(
widget.tabNumber, widget.tabNumber,
(int index) => FocusScopeNode(), (int index) => FocusScopeNode(debugLabel: 'Tab Focus Scope $index'),
); );
} }
...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> { ...@@ -327,7 +327,7 @@ class _TabSwitchingViewState extends State<_TabSwitchingView> {
@override @override
void dispose() { void dispose() {
for (FocusScopeNode focusScopeNode in tabFocusNodes) { for (FocusScopeNode focusScopeNode in tabFocusNodes) {
focusScopeNode.detach(); focusScopeNode.dispose();
} }
super.dispose(); super.dispose();
} }
......
...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> { ...@@ -191,7 +191,7 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showSuggestions] to show the search suggestions again. /// * [showSuggestions] to show the search suggestions again.
void showResults(BuildContext context) { void showResults(BuildContext context) {
_focusNode.unfocus(); _focusNode?.unfocus();
_currentBody = _SearchBody.results; _currentBody = _SearchBody.results;
} }
...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> { ...@@ -208,7 +208,8 @@ abstract class SearchDelegate<T> {
/// ///
/// * [showResults] to show the search results. /// * [showResults] to show the search results.
void showSuggestions(BuildContext context) { void showSuggestions(BuildContext context) {
FocusScope.of(context).requestFocus(_focusNode); assert(_focusNode != null, '_focusNode must be set by route before showSuggestions is called.');
_focusNode.requestFocus();
_currentBody = _SearchBody.suggestions; _currentBody = _SearchBody.suggestions;
} }
...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> { ...@@ -218,7 +219,7 @@ abstract class SearchDelegate<T> {
/// to [showSearch] that launched the search initially. /// to [showSearch] that launched the search initially.
void close(BuildContext context, T result) { void close(BuildContext context, T result) {
_currentBody = null; _currentBody = null;
_focusNode.unfocus(); _focusNode?.unfocus();
Navigator.of(context) Navigator.of(context)
..popUntil((Route<dynamic> route) => route == _route) ..popUntil((Route<dynamic> route) => route == _route)
..pop(result); ..pop(result);
...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> { ...@@ -232,7 +233,9 @@ abstract class SearchDelegate<T> {
/// page. /// page.
Animation<double> get transitionAnimation => _proxyAnimation; Animation<double> get transitionAnimation => _proxyAnimation;
final FocusNode _focusNode = FocusNode(); // 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 TextEditingController _queryTextController = TextEditingController(); final TextEditingController _queryTextController = TextEditingController();
...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> { ...@@ -246,7 +249,6 @@ abstract class SearchDelegate<T> {
} }
_SearchPageRoute<T> _route; _SearchPageRoute<T> _route;
} }
/// Describes the body that is currently shown under the [AppBar] in the /// Describes the body that is currently shown under the [AppBar] in the
...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget { ...@@ -346,13 +348,18 @@ class _SearchPage<T> extends StatefulWidget {
} }
class _SearchPageState<T> extends State<_SearchPage<T>> { 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 @override
void initState() { void initState() {
super.initState(); super.initState();
queryTextController.addListener(_onQueryChanged); queryTextController.addListener(_onQueryChanged);
widget.animation.addStatusListener(_onAnimationStatusChanged); widget.animation.addStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.addListener(_onSearchBodyChanged);
widget.delegate._focusNode.addListener(_onFocusChanged); focusNode.addListener(_onFocusChanged);
widget.delegate._focusNode = focusNode;
} }
@override @override
...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -361,7 +368,8 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
queryTextController.removeListener(_onQueryChanged); queryTextController.removeListener(_onQueryChanged);
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate._currentBodyNotifier.removeListener(_onSearchBodyChanged);
widget.delegate._focusNode.removeListener(_onFocusChanged); widget.delegate._focusNode = null;
focusNode.dispose();
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _onAnimationStatusChanged(AnimationStatus status) {
...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -370,12 +378,12 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
} }
widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.animation.removeStatusListener(_onAnimationStatusChanged);
if (widget.delegate._currentBody == _SearchBody.suggestions) { if (widget.delegate._currentBody == _SearchBody.suggestions) {
FocusScope.of(context).requestFocus(widget.delegate._focusNode); focusNode.requestFocus();
} }
} }
void _onFocusChanged() { void _onFocusChanged() {
if (widget.delegate._focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) { if (focusNode.hasFocus && widget.delegate._currentBody != _SearchBody.suggestions) {
widget.delegate.showSuggestions(context); widget.delegate.showSuggestions(context);
} }
} }
...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> { ...@@ -436,7 +444,7 @@ class _SearchPageState<T> extends State<_SearchPage<T>> {
leading: widget.delegate.buildLeading(context), leading: widget.delegate.buildLeading(context),
title: TextField( title: TextField(
controller: queryTextController, controller: queryTextController,
focusNode: widget.delegate._focusNode, focusNode: focusNode,
style: theme.textTheme.title, style: theme.textTheme.title,
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
onSubmitted: (String _) { onSubmitted: (String _) {
......
...@@ -1465,7 +1465,8 @@ class RenderEditable extends RenderBox { ...@@ -1465,7 +1465,8 @@ class RenderEditable extends RenderBox {
} }
TextSelection _selectWordAtOffset(TextPosition position) { TextSelection _selectWordAtOffset(TextPosition position) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final TextRange word = _textPainter.getWordBoundary(position); final TextRange word = _textPainter.getWordBoundary(position);
// When long-pressing past the end of the text, we want a collapsed cursor. // When long-pressing past the end of the text, we want a collapsed cursor.
if (position.offset >= word.end) if (position.offset >= word.end)
...@@ -1537,7 +1538,8 @@ class RenderEditable extends RenderBox { ...@@ -1537,7 +1538,8 @@ class RenderEditable extends RenderBox {
} }
void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) { void _paintCaret(Canvas canvas, Offset effectiveOffset, TextPosition textPosition) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
// If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while // If the floating cursor is enabled, the text cursor's color is [backgroundCursorColor] while
// the floating cursor's color is _cursorColor; // the floating cursor's color is _cursorColor;
...@@ -1603,7 +1605,8 @@ class RenderEditable extends RenderBox { ...@@ -1603,7 +1605,8 @@ class RenderEditable extends RenderBox {
} }
void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) { void _paintFloatingCaret(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_floatingCursorOn); assert(_floatingCursorOn);
// We always want the floating cursor to render at full opacity. // We always want the floating cursor to render at full opacity.
...@@ -1690,7 +1693,8 @@ class RenderEditable extends RenderBox { ...@@ -1690,7 +1693,8 @@ class RenderEditable extends RenderBox {
} }
void _paintSelection(Canvas canvas, Offset effectiveOffset) { void _paintSelection(Canvas canvas, Offset effectiveOffset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
assert(_selectionRects != null); assert(_selectionRects != null);
final Paint paint = Paint()..color = _selectionColor; final Paint paint = Paint()..color = _selectionColor;
for (ui.TextBox box in _selectionRects) for (ui.TextBox box in _selectionRects)
...@@ -1698,7 +1702,8 @@ class RenderEditable extends RenderBox { ...@@ -1698,7 +1702,8 @@ class RenderEditable extends RenderBox {
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
assert(_textLayoutLastWidth == constraints.maxWidth); assert(_textLayoutLastWidth == constraints.maxWidth,
'Last width ($_textLayoutLastWidth) not the same as max width constraint (${constraints.maxWidth}).');
final Offset effectiveOffset = offset + _paintOffset; final Offset effectiveOffset = offset + _paintOffset;
bool showSelection = false; bool showSelection = false;
......
...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart'; ...@@ -90,7 +90,7 @@ import 'package:flutter/foundation.dart';
/// onTap: () { /// onTap: () {
/// FocusScope.of(context).requestFocus(_focusNode); /// FocusScope.of(context).requestFocus(_focusNode);
/// }, /// },
/// child: Text('Tap to focus'), /// child: const Text('Tap to focus'),
/// ); /// );
/// } /// }
/// return Text(_message ?? 'Press a key'); /// return Text(_message ?? 'Press a key');
......
...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -787,6 +787,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final LayerLink _layerLink = LayerLink(); final LayerLink _layerLink = LayerLink();
bool _didAutoFocus = false; bool _didAutoFocus = false;
FocusAttachment _focusAttachment;
// This value is an eyeball estimation of the time it takes for the iOS cursor // This value is an eyeball estimation of the time it takes for the iOS cursor
// to ease in and out. // to ease in and out.
...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -809,6 +810,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
void initState() { void initState() {
super.initState(); super.initState();
widget.controller.addListener(_didChangeTextEditingValue); widget.controller.addListener(_didChangeTextEditingValue);
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); }); _scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
_cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration); _cursorBlinkOpacityController = AnimationController(vsync: this, duration: _fadeDuration);
...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -836,6 +838,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
if (widget.focusNode != oldWidget.focusNode) { if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged); oldWidget.focusNode.removeListener(_handleFocusChanged);
_focusAttachment?.detach();
_focusAttachment = widget.focusNode.attach(context);
widget.focusNode.addListener(_handleFocusChanged); widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive(); updateKeepAlive();
} }
...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -852,6 +856,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(_cursorTimer == null); assert(_cursorTimer == null);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
_focusAttachment.detach();
widget.focusNode.removeListener(_handleFocusChanged); widget.focusNode.removeListener(_handleFocusChanged);
super.dispose(); super.dispose();
} }
...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1091,10 +1096,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
if (_hasFocus) { if (_hasFocus) {
_openInputConnection(); _openInputConnection();
} else { } else {
final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context); widget.focusNode.requestFocus();
for (int i = ancestorScopes.length - 1; i >= 1; i -= 1)
ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]);
FocusScope.of(context).requestFocus(widget.focusNode);
} }
} }
...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -1400,7 +1402,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
FocusScope.of(context).reparentIfNeeded(widget.focusNode); _focusAttachment.reparent();
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls; final TextSelectionControls controls = widget.selectionControls;
......
...@@ -3,61 +3,505 @@ ...@@ -3,61 +3,505 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/services.dart';
/// A leaf node in the focus tree that can receive focus. 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 focus tree keeps track of which widget is the user's current focus. The /// The [node] is the node that received the event.
/// focused widget often listens for keyboard events. typedef FocusOnKeyCallback = bool Function(FocusNode node, RawKeyEvent event);
/// An attachment point for a [FocusNode].
/// ///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext] /// Once created, a [FocusNode] must be attached to the widget tree by its
/// and call the [FocusScopeNode.requestFocus] method: /// _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.
/// ///
/// ```dart /// This attachment is created by calling [FocusNode.attach], usually from the
/// FocusScope.of(context).requestFocus(focusNode); /// 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).
///
/// 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).
///
/// 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}
///
/// {@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';
/// ``` /// ```
/// ///
/// If your widget requests focus, be sure to call /// ```dart preamble
/// `FocusScope.of(context).reparentIfNeeded(focusNode);` in your `build` /// class ColorfulButton extends StatefulWidget {
/// method to reparent your [FocusNode] if your widget moves from one /// ColorfulButton({Key key}) : super(key: key);
/// location in the tree to another. ///
/// @override
/// _ColorfulButtonState createState() => _ColorfulButtonState();
/// }
/// ///
/// ## Lifetime /// class _ColorfulButtonState extends State<ColorfulButton> {
/// FocusNode _node;
/// FocusAttachment _nodeAttachment;
/// Color _color = Colors.white;
/// ///
/// Focus nodes are long-lived objects. For example, if a stateful widget has a /// @override
/// focusable child widget, it should create a [FocusNode] in the /// void initState() {
/// [State.initState] method, and [dispose] it in the [State.dispose] method, /// super.initState();
/// providing the same [FocusNode] to the focusable child each time the /// _node = FocusNode(debugLabel: 'Button');
/// [State.build] method is run. In particular, creating a [FocusNode] each time /// _nodeAttachment = _node.attach(context, onKey: _handleKeyPress);
/// [State.build] is invoked will cause the focus to be lost each time the /// }
/// widget is built. ///
/// 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();
/// }
///
/// @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'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// ```dart
/// Widget build(BuildContext context) {
/// final TextTheme textTheme = Theme.of(context).textTheme;
/// return DefaultTextStyle(
/// style: textTheme.display1,
/// child: ColorfulButton(),
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
/// * [FocusScopeNode], which is an interior node in the focus tree. /// * [Focus], a widget that manages a [FocusNode] and provides access to
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given /// focus information and actions to its descendant widgets.
/// [BuildContext]. /// * [FocusScope], a widget that manages a [FocusScopeNode] and provides
class FocusNode extends ChangeNotifier { /// access to scope information and actions to its descendant widgets.
FocusScopeNode _parent; /// * [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;
FocusManager _manager; FocusManager _manager;
bool _hasKeyboardToken = false; bool _hasKeyboardToken = false;
/// Whether this node has the overall focus. /// Returns the parent node for this object.
///
/// 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.
///
/// 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.
///
/// 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 overall focus when the node is focused in its /// A [FocusNode] has the primary focus when the node is focused in its
/// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for /// nearest ancestor [FocusScopeNode] and [hasFocus] is true for all its
/// that scope and all its ancestor scopes. /// ancestor nodes, but none of its descendants.
/// ///
/// To request focus, find the [FocusScopeNode] for the current [BuildContext] /// This is different from [hasFocus] in that [hasFocus] is true if the node
/// and call the [FocusScopeNode.requestFocus] method: /// is anywhere in the focus chain, but here the node has to be at the end of
/// the chain to return true.
/// ///
/// ```dart /// A node that returns true for [hasPrimaryFocus] will be the first node to
/// FocusScope.of(context).requestFocus(focusNode); /// receive key events through its [onKey] handler.
/// ```
/// ///
/// This object notifies its listeners whenever this value changes. /// This object notifies its listeners whenever this value changes.
bool get hasFocus => _manager?._currentFocus == this; 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);
}
/// Removes the keyboard token from this focus node if it has one. /// Removes the keyboard token from this focus node if it has one.
/// ///
...@@ -68,330 +512,347 @@ class FocusNode extends ChangeNotifier { ...@@ -68,330 +512,347 @@ class FocusNode extends ChangeNotifier {
/// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus /// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus
/// node receives a keyboard token if it does not already have one. Later, /// node receives a keyboard token if it does not already have one. Later,
/// when the focus node becomes focused, the widget that manages the /// 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 /// [TextInputConnection.show]) only if it successfully consumes the keyboard
/// token from the focus node. /// token from the focus node.
/// ///
/// Returns whether this function successfully consumes a keyboard token. /// Returns true if this method successfully consumes the keyboard token.
bool consumeKeyboardToken() { bool consumeKeyboardToken() {
if (!_hasKeyboardToken) if (!_hasKeyboardToken) {
return false; return false;
}
_hasKeyboardToken = false; _hasKeyboardToken = false;
return true; return true;
} }
/// Cancels any outstanding requests for focus. // 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.
/// This method is safe to call regardless of whether this node has ever void _markAsDirty({FocusNode newFocus}) {
/// requested focus. if (_manager != null) {
void unfocus() { // If we have a manager, then let it handle the focus change.
_parent?._resignFocus(this); _manager._dirtyNodes?.add(this);
assert(_parent == null); _manager._markNeedsUpdate(newFocus: newFocus);
assert(_manager == null); } else {
// If we don't have a manager, then change the focus locally.
newFocus?._setAsFocusedChild();
newFocus?._notify();
if (newFocus != this) {
_notify();
} }
@override
void dispose() {
_manager?._willDisposeFocusNode(this);
_parent?._resignFocus(this);
assert(_parent == null);
assert(_manager == null);
super.dispose();
} }
void _notify() {
notifyListeners();
} }
@override // Removes the given FocusNode and its children as a child of this node.
String toString() => '${describeIdentity(this)}${hasFocus ? '(FOCUSED)' : ''}'; @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);
/// An interior node in the focus tree. // If the child was (or requested to be) the primary focus, then unfocus it
/// // and cancel any outstanding request to be focused.
/// The focus tree keeps track of which widget is the user's current focus. The node.unfocus();
/// focused widget often listens for keyboard events.
///
/// 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.
///
/// 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.
///
/// If a [FocusScopeNode] is removed, then the next sibling node will be set as
/// the focused node by the [FocusManager].
///
/// See also:
///
/// * [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;
FocusScopeNode _nextSibling;
FocusScopeNode _previousSibling;
FocusScopeNode _firstChild;
FocusScopeNode _lastChild;
FocusNode _focus;
List<FocusScopeNode> _focusPath;
/// Whether this scope is currently active in its parent scope. node._parent = null;
bool get isFirstFocus => _parent == null || _parent._firstChild == this; _children.remove(node);
assert(_manager == null || !_manager.rootScope.descendants.contains(node));
}
// Returns this FocusScopeNode's ancestors, starting with the node void _updateManager(FocusManager manager) {
// below the FocusManager's rootScope. _manager = manager;
List<FocusScopeNode> _getFocusPath() { for (FocusNode descendant in descendants) {
final List<FocusScopeNode> nodes = <FocusScopeNode>[this]; descendant._manager = manager;
FocusScopeNode node = _parent;
while (node != null && node != _manager?.rootScope) {
nodes.add(node);
node = node._parent;
} }
return nodes;
} }
void _prepend(FocusScopeNode child) { // Used by FocusAttachment.reparent to perform the actual parenting operation.
assert(child != this); @mustCallSuper
assert(child != _firstChild); void _reparent(FocusNode child) {
assert(child != _lastChild); assert(child != null);
assert(child._parent == null); assert(child != this, 'Tried to make a child into a parent of itself.');
assert(child._manager == null); if (child._parent == this) {
assert(child._nextSibling == null); assert(_children.contains(child), "Found a node that says it's a child, but doesn't appear in the child list.");
assert(child._previousSibling == null); // The child is already a child of this parent.
assert(() { return;
FocusScopeNode node = this; }
while (node._parent != null) assert(_manager == null || child != _manager.rootScope, "Reparenting the root node isn't allowed.");
node = node._parent; assert(!ancestors.contains(child), 'The supplied child is already an ancestor of this node. Loops are not allowed.');
assert(node != child); // indicates we are about to create a cycle FocusNode oldPrimaryFocus;
return true; 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._parent = this;
child._nextSibling = _firstChild;
if (_firstChild != null)
_firstChild._previousSibling = child;
_firstChild = child;
_lastChild ??= child;
child._updateManager(_manager); 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();
}
} }
void _updateManager(FocusManager manager) { /// Called by the _host_ [StatefulWidget] to attach a [FocusNode] to the
void update(FocusScopeNode child) { /// widget tree.
if (child._manager == manager) ///
return; /// In order to attach a [FocusNode] to the widget tree, call [attach],
child._manager = manager; /// typically from the [StatefulWidget]'s [State.initState] method.
// We don't proactively null out the manager for FocusNodes because the ///
// manager holds the currently active focus node until the end of the /// If the focus node in the host widget is swapped out, the new node will
// microtask, even if that node is detached from the focus tree. /// need to be attached. [FocusAttachment.detach] should be called on the old
if (manager != null) /// node, and then [attach] called on the new node. This typically happens in
child._focus?._manager = manager; /// the [State.didUpdateWidget] method.
child._visitChildren(update); @mustCallSuper
FocusAttachment attach(BuildContext context, {FocusOnKeyCallback onKey}) {
_context = context;
_onKey = onKey;
_attachment = FocusAttachment._(this);
return _attachment;
} }
update(this); @override
void dispose() {
_manager?._willDisposeFocusNode(this);
_attachment?.detach();
super.dispose();
} }
void _visitChildren(void visitor(FocusScopeNode child)) { @mustCallSuper
FocusScopeNode child = _firstChild; void _notify() {
while (child != null) { if (_parent == null) {
visitor(child); // no longer part of the tree, so don't notify.
child = child._nextSibling; return;
} }
if (hasPrimaryFocus) {
_setAsFocusedChild();
}
notifyListeners();
} }
bool _debugUltimatePreviousSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) { /// Requests the primary focus for this node, or for a supplied [node], which
while (child._previousSibling != null) { /// will also give focus to its [ancestors].
assert(child._previousSibling != child); ///
child = child._previousSibling; /// 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);
} }
return child == equals; 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);
} }
bool _debugUltimateNextSiblingOf(FocusScopeNode child, { FocusScopeNode equals }) { // Note that this is overridden in FocusScopeNode.
while (child._nextSibling != null) { void _doRequestFocus({@required bool isFromPolicy}) {
assert(child._nextSibling != child); assert(isFromPolicy != null);
child = child._nextSibling; _setAsFocusedChild();
if (hasPrimaryFocus) {
return;
} }
return child == equals; _hasKeyboardToken = true;
_markAsDirty(newFocus: this);
} }
void _remove(FocusScopeNode child) { // Sets this node as the focused child for the enclosing scope, and that scope
assert(child._parent == this); // as the focused child for the scope above it, etc., until it reaches the
assert(child._manager == _manager); // root node. It doesn't change the primary focus, it just changes what node
assert(_debugUltimatePreviousSiblingOf(child, equals: _firstChild)); // would be focused if the enclosing scope receives focus, and keeps track of
assert(_debugUltimateNextSiblingOf(child, equals: _lastChild)); // previously focused children so that if one is removed, the previous focus
if (child._previousSibling == null) { // returns.
assert(_firstChild == child); void _setAsFocusedChild() {
_firstChild = child._nextSibling; FocusNode scopeFocus = this;
} else { for (FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) {
child._previousSibling._nextSibling = child._nextSibling; 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;
} }
if (child._nextSibling == null) {
assert(_lastChild == child);
_lastChild = child._previousSibling;
} else {
child._nextSibling._previousSibling = child._previousSibling;
} }
child._previousSibling = null;
child._nextSibling = null; @override
child._parent = null; void debugFillProperties(DiagnosticPropertiesBuilder properties) {
child._updateManager(null); 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));
} }
void _didChangeFocusChain() { @override
if (isFirstFocus) List<DiagnosticsNode> debugDescribeChildren() {
_manager?._markNeedsUpdate(); int count = 1;
return _children.map<DiagnosticsNode>((FocusNode child) {
return child.toDiagnosticsNode(name: 'Child ${count++}');
}).toList();
} }
}
/// Requests that the given node becomes the focus for this scope. /// A subclass of [FocusNode] that acts as a scope for its descendants,
/// /// maintaining information about which descendant is currently or was last
/// If the given node is currently focused in another scope, the node will /// focused.
/// first be unfocused in that scope. ///
/// _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._
///
/// [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.
///
/// 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.
///
/// {@macro flutter.widgets.focusManager.lifecycle}
/// {@macro flutter.widgets.focus_manager.focus.keyEvents}
///
/// 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.
/// ///
/// The node will receive the overall focus if this [isFirstFocus] is true /// All parameters are optional.
/// in this scope and all its ancestor scopes. The node is notified that it FocusScopeNode({
/// has received the overall focus in a microtask. String debugLabel,
void requestFocus(FocusNode node) { FocusOnKeyCallback onKey,
assert(node != null); }) : super(debugLabel: debugLabel, onKey: onKey);
if (_focus == node && listEquals<FocusScopeNode>(_focusPath, _manager?._getCurrentFocusPath()))
return; @override
_focus?.unfocus(); FocusScopeNode get nearestScope => this;
node._hasKeyboardToken = true;
_setFocus(node);
}
/// If this scope lacks a focus, request that the given node becomes the /// Returns true if this scope is the focused child of its parent scope.
/// focus. bool get isFirstFocus => enclosingScope.focusedChild == this;
/// Returns the child of this node that should receive focus if this scope
/// node receives focus.
/// ///
/// Useful for widgets that wish to grab the focus if no other widget already /// If [hasFocus] is true, then this points to the child of this node that is
/// has the focus. /// currently focused.
/// ///
/// The node is notified that it has received the overall focus in a /// Returns null if there is no currently focused child.
/// microtask. FocusNode get focusedChild {
void autofocus(FocusNode node) { assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this,
assert(node != null); 'Focused child does not have the same idea of its enclosing scope as the scope does.');
if (_focus == null) { return _focusedChildren.isNotEmpty ? _focusedChildren.last : null;
node._hasKeyboardToken = true;
_setFocus(node);
}
} }
/// Adopts the given node if it is focused in another scope. // 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>[];
/// Make the given [scope] the active child scope for this scope.
/// ///
/// A widget that requests that a node is focused should call this method /// If the given [scope] is not yet a part of the focus tree, then add it to
/// during its `build` method in case the widget is moved from one location /// the tree as a child of this scope. If it is already part of the focus
/// in the tree to another location that has a different focus scope. /// tree, the given scope must be a descendant of this scope.
void reparentIfNeeded(FocusNode node) { void setFirstFocus(FocusScopeNode scope) {
assert(node != null); assert(scope != null);
if (node._parent == null || node._parent == this) if (scope._parent == null) {
return; _reparent(scope);
node.unfocus();
assert(node._parent == null);
if (_focus == null)
_setFocus(node);
} }
assert(scope.ancestors.contains(this), '$FocusScopeNode $scope must be a child of $this to set it as first focus.');
void _setFocus(FocusNode node) { // Move down the tree, checking each focusedChild until we get to a node
assert(node != null); // that either isn't a scope node, or has no focused child, and then request
assert(node._parent == null); // focus on that node.
assert(_focus == null); FocusNode descendantFocus = scope.focusedChild;
_focus = node; while (descendantFocus is FocusScopeNode && descendantFocus != null) {
_focus._parent = this; final FocusScopeNode descendantScope = descendantFocus;
_focus._manager = _manager; descendantFocus = descendantScope.focusedChild;
_focus._hasKeyboardToken = true;
_focusPath = _getFocusPath();
_didChangeFocusChain();
} }
if (descendantFocus != null) {
void _resignFocus(FocusNode node) { descendantFocus?._doRequestFocus(isFromPolicy: false);
assert(node != null); } else {
if (_focus != node) scope._doRequestFocus(isFromPolicy: false);
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. /// If this scope lacks a focus, request that the given node become the focus.
/// ///
/// A widget that sets a scope as the first focus of another scope should /// If the given node is not yet part of the focus tree, then add it as a
/// call this method during its `build` method in case the widget is moved /// child of this node.
/// 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 /// Useful for widgets that wish to grab the focus if no other widget already
/// is simply detached from its old parent. /// has the focus.
void reparentScopeIfNeeded(FocusScopeNode child) { ///
assert(child != null); /// The node is notified that it has received the primary focus in a
if (child._parent == null || child._parent == this) /// microtask, so notification may lag the request by up to one frame.
return; void autofocus(FocusNode node) {
if (child.isFirstFocus) { if (focusedChild == null) {
setFirstFocus(child); if (node._parent == null) {
} else { _reparent(node);
child.detach();
} }
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._doRequestFocus(isFromPolicy: false);
} }
/// 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 @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void _doRequestFocus({@required bool isFromPolicy}) {
super.debugFillProperties(properties); assert(isFromPolicy != null);
if (_focus != null) // Start with the primary focus as the focused child of this scope, if there
properties.add(DiagnosticsProperty<FocusNode>('focus', _focus)); // 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) {
@override // We didn't find a FocusNode at the leaf, so we're focusing the scope.
List<DiagnosticsNode> debugDescribeChildren() { _markAsDirty(newFocus: primaryFocus);
final List<DiagnosticsNode> children = <DiagnosticsNode>[]; } else {
if (_firstChild != null) { primaryFocus.requestFocus();
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;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<FocusNode>('focusedChild', focusedChild, defaultValue: null));
} }
} }
...@@ -418,70 +879,162 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -418,70 +879,162 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// ///
/// See also: /// See also:
/// ///
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// * [FocusNode], which is a node in the focus tree that can receive focus.
/// focus. /// * [FocusScopeNode], which is an node in the focus tree used to collect
/// * [FocusScopeNode], which is an interior node in the focus tree. /// subtrees into groups.
/// * [FocusScope.of], which provides the [FocusScopeNode] for a given /// * [Focus.of], which provides the nearest ancestor [FocusNode] for a given
/// [BuildContext]. /// [BuildContext].
class FocusManager { /// * [FocusScope.of], which provides the nearest ancestor [FocusScopeNode] for
/// a given [BuildContext].
class FocusManager with DiagnosticableTreeMixin {
/// Creates an object that manages the focus tree. /// Creates an object that manages the focus tree.
/// ///
/// This constructor is rarely called directly. To access the [FocusManager], /// This constructor is rarely called directly. To access the [FocusManager],
/// consider using [WidgetsBinding.focusManager] instead. /// consider using [WidgetsBinding.focusManager] instead.
FocusManager() { FocusManager() {
rootScope._manager = this; rootScope._manager = this;
assert(rootScope._firstChild == null); RawKeyboard.instance.addListener(_handleRawKeyEvent);
assert(rootScope._lastChild == null);
} }
/// The root [FocusScopeNode] in the focus tree. /// The root [FocusScopeNode] in the focus tree.
/// ///
/// This field is rarely used directly. Instead, to find the /// This field is rarely used directly. To find the nearest [FocusScopeNode]
/// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of]. /// for a given [FocusNode], call [FocusNode.nearestScope].
final FocusScopeNode rootScope = FocusScopeNode(); final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
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;
}
}
}
// The node that currently has the primary focus.
FocusNode _currentFocus; 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) { void _willDisposeFocusNode(FocusNode node) {
assert(node != null); assert(node != null);
if (_currentFocus == node) _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) {
_currentFocus = null; _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; bool _haveScheduledUpdate = false;
void _markNeedsUpdate() {
if (_haveScheduledUpdate) // 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) {
return; return;
_haveScheduledUpdate = true;
scheduleMicrotask(_update);
} }
_haveScheduledUpdate = true;
FocusNode _findNextFocus() { scheduleMicrotask(_applyFocusChange);
FocusScopeNode scope = rootScope;
while (scope._firstChild != null)
scope = scope._firstChild;
return scope._focus;
} }
void _update() { void _applyFocusChange() {
_haveScheduledUpdate = false; _haveScheduledUpdate = false;
final FocusNode nextFocus = _findNextFocus();
if (_currentFocus == nextFocus)
return;
final FocusNode previousFocus = _currentFocus; final FocusNode previousFocus = _currentFocus;
_currentFocus = nextFocus; if (_currentFocus == null && _nextFocus == null) {
previousFocus?._notify(); _nextFocus = rootScope;
_currentFocus?._notify(); }
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();
} }
List<FocusScopeNode> _getCurrentFocusPath() => _currentFocus?._parent?._getFocusPath(); @override
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
rootScope.toDiagnosticsNode(name: 'rootScope'),
];
}
@override @override
String toString() { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED'));
const String indent = ' '; properties.add(DiagnosticsProperty<FocusNode>('currentFocus', _currentFocus, defaultValue: null));
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;
}());
}
// Copyright 2015 The Chromium Authors. All rights reserved. // Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'framework.dart'; import 'framework.dart';
import 'inherited_notifier.dart';
class _FocusScopeMarker extends InheritedWidget { /// A widget that manages a [FocusNode] to allow keyboard focus to be given
const _FocusScopeMarker({ /// to this widget and its descendants.
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.
/// ///
/// The focus tree keeps track of which widget is the user's current focus. The /// When the focus is gained or lost, [onFocusChanged] is called.
/// focused widget often listens for keyboard events.
/// ///
/// A focus scope does not itself receive focus but instead helps remember /// For keyboard events, [onKey] is called if [FocusNode.hasFocus] is true for
/// previous focus states. A scope is currently active when its [node] is the /// this widget's [focusNode], unless a focused descendant's [onKey] callback
/// first focus of its parent scope. To activate a [FocusScope], either use the /// returns false when called.
/// [autofocus] property or explicitly make the [node] the first focus in the
/// parent scope:
/// ///
/// ```dart /// This widget does not provide any visual indication that the focus has
/// FocusScope.of(context).setFirstFocus(node); /// 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].
///
/// {@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].
///
/// ```dart imports
/// import 'package:flutter/services.dart';
/// ``` /// ```
/// ///
/// If a [FocusScope] is removed from the widget tree, then the previously /// ```dart
/// focused node will be focused, but only if the [node] is the same [node] /// Color _color = Colors.white;
/// 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, /// bool _handleKeyPress(FocusNode node, RawKeyEvent event) {
/// or pass in the [node] from a parent that is not rebuilt. If there is no next /// if (event is RawKeyDownEvent) {
/// sibling, then the parent scope node will be focused. /// 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'),
/// ),
/// ),
/// );
/// },
/// ),
/// ),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
/// ///
/// See also: /// See also:
/// ///
/// * [FocusScopeNode], which is the associated node in the focus tree. /// * [FocusNode], which represents a node in the focus hierarchy and
/// * [FocusNode], which is a leaf node in the focus tree that can receive /// [FocusNode]'s API documentation includes a detailed explanation of its
/// focus. /// role in the overall focus system.
class FocusScope extends StatefulWidget { /// * [FocusScope], a widget that manages a group of focusable widgets using a
/// Creates a scope in which widgets can receive focus. /// [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 [node] argument must not be null. /// The [child] argument is required and must not be null.
const FocusScope({ ///
/// The [autofocus] argument must not be null.
const Focus({
Key key, Key key,
@required this.node, @required this.child,
this.focusNode,
this.autofocus = false, this.autofocus = false,
this.child, this.onFocusChange,
}) : assert(node != null), this.onKey,
this.debugLabel,
}) : assert(child != null),
assert(autofocus != null), assert(autofocus != null),
super(key: key); super(key: key);
/// Controls whether this scope is currently active. /// A debug label for this widget.
final FocusScopeNode node; ///
/// Not used for anything except to be printed in the diagnostic output from
/// Whether this scope should attempt to become active when first added to /// [toString] or [toStringDeep]. Also unused if a [focusNode] is provided,
/// the tree. /// since that node can have its own [FocusNode.debugLabel].
final bool autofocus; ///
/// 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 widget below this widget in the tree. /// The child widget of this [Focus].
/// ///
/// {@macro flutter.widgets.child} /// {@macro flutter.widgets.child}
final Widget child; final Widget child;
/// Returns the [node] of the [FocusScope] that most tightly encloses the /// Handler for keys pressed when this object or one of its children has
/// given [BuildContext]. /// focus.
/// ///
/// The [context] argument must not be null. /// Key events are first given to the [FocusNode] that has primary focus, and
static FocusScopeNode of(BuildContext context) { /// if its [onKey] method return false, then they are given to each ancestor
assert(context != null); /// node up the focus hierarchy in turn. If an event reaches the root of the
final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker); /// hierarchy, it is discarded.
return scope?.node ?? context.owner.focusManager.rootScope; ///
} /// 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;
/// 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.
final bool autofocus;
/// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of /// An optional focus node to use as the focus node for this [Focus] widget.
/// the given [BuildContext]. The first element of the list is the
/// nearest ancestor's [FocusScopeNode].
/// ///
/// The returned list does not include the [FocusManager]'s `rootScope`. /// If one is not supplied, then one will be allocated and owned by this
/// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are /// widget.
/// returned. ///
/// 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;
/// 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.
/// ///
/// The [context] argument must not be null. /// The [context] argument must not be null.
static List<FocusScopeNode> ancestorsOf(BuildContext context) { static FocusNode of(BuildContext context) {
assert(context != null); assert(context != null);
final List<FocusScopeNode> ancestors = <FocusScopeNode>[]; final _FocusMarker marker = context.inheritFromWidgetOfExactType(_FocusMarker);
while (true) { return marker?.notifier ?? context.owner.focusManager.rootScope;
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;
});
} }
/// Returns true if the nearest enclosing [Focus] widget's node is focused.
///
/// 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));
} }
@override @override
_FocusScopeState createState() => _FocusScopeState(); _FocusState createState() => _FocusState();
} }
class _FocusScopeState extends State<FocusScope> { class _FocusState extends State<Focus> {
FocusNode _internalNode;
FocusNode get node => widget.focusNode ?? _internalNode;
bool _hasFocus;
bool _didAutofocus = false; 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 @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_focusAttachment?.reparent();
if (!_didAutofocus && widget.autofocus) { if (!_didAutofocus && widget.autofocus) {
FocusScope.of(context).setFirstFocus(widget.node); FocusScope.of(context).autofocus(node);
_didAutofocus = true; _didAutofocus = true;
} }
} }
@override @override
void dispose() { void deactivate() {
widget.node.detach(); super.deactivate();
super.dispose(); _didAutofocus = false;
}
@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,
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FocusScope.of(context).reparentScopeIfNeeded(widget.node); _focusAttachment.reparent();
return Semantics( return Semantics(
explicitChildNodes: true, explicitChildNodes: true,
child: _FocusScopeMarker( child: _FocusMarker(
node: widget.node, node: node,
child: widget.child, 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);
}
...@@ -2117,7 +2117,7 @@ class BuildOwner { ...@@ -2117,7 +2117,7 @@ class BuildOwner {
/// the [FocusScopeNode] for a given [BuildContext]. /// the [FocusScopeNode] for a given [BuildContext].
/// ///
/// See [FocusManager] for more details. /// See [FocusManager] for more details.
final FocusManager focusManager = FocusManager(); FocusManager focusManager = FocusManager();
/// Adds an element to the dirty elements list so that it will be rebuilt /// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope]. /// when [WidgetsBinding.drawFrame] calls [buildScope].
......
...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1468,7 +1468,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{}; final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{};
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes. /// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[]; final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[];
...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1556,7 +1556,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
route.dispose(); route.dispose();
_poppedRoutes.clear(); _poppedRoutes.clear();
_history.clear(); _history.clear();
focusScopeNode.detach(); focusScopeNode.dispose();
super.dispose(); super.dispose();
assert(() { _debugLocked = false; return true; }()); assert(() { _debugLocked = false; return true; }());
} }
......
...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; ...@@ -7,6 +7,7 @@ import 'package:flutter/services.dart';
import 'basic.dart'; import 'basic.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart'; import 'framework.dart';
export 'package:flutter/services.dart' show RawKeyEvent; export 'package:flutter/services.dart' show RawKeyEvent;
...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> { ...@@ -112,5 +113,5 @@ class _RawKeyboardListenerState extends State<RawKeyboardListener> {
} }
@override @override
Widget build(BuildContext context) => widget.child; Widget build(BuildContext context) => Focus(focusNode: widget.focusNode, child: widget.child);
} }
...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -583,6 +583,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
// This is the combination of the two animations for the route. // This is the combination of the two animations for the route.
Listenable _listenable; Listenable _listenable;
/// The node this scope will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -592,12 +595,18 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -592,12 +595,18 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
if (widget.route.secondaryAnimation != null) if (widget.route.secondaryAnimation != null)
animations.add(widget.route.secondaryAnimation); animations.add(widget.route.secondaryAnimation);
_listenable = Listenable.merge(animations); _listenable = Listenable.merge(animations);
if (widget.route.isCurrent) {
widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
}
} }
@override @override
void didUpdateWidget(_ModalScope<T> oldWidget) { void didUpdateWidget(_ModalScope<T> oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
assert(widget.route == oldWidget.route); assert(widget.route == oldWidget.route);
if (widget.route.isCurrent) {
widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
}
} }
@override @override
...@@ -612,6 +621,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -612,6 +621,12 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
}); });
} }
@override
void dispose() {
focusScopeNode.dispose();
super.dispose();
}
// This should be called to wrap any changes to route.isCurrent, route.canPop, // This should be called to wrap any changes to route.isCurrent, route.canPop,
// and route.offstage. // and route.offstage.
void _routeSetState(VoidCallback fn) { void _routeSetState(VoidCallback fn) {
...@@ -629,7 +644,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -629,7 +644,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
child: PageStorage( child: PageStorage(
bucket: widget.route._storageBucket, // immutable bucket: widget.route._storageBucket, // immutable
child: FocusScope( child: FocusScope(
node: widget.route.focusScopeNode, // immutable node: focusScopeNode, // immutable
child: RepaintBoundary( child: RepaintBoundary(
child: AnimatedBuilder( child: AnimatedBuilder(
animation: _listenable, // immutable animation: _listenable, // immutable
...@@ -887,9 +902,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -887,9 +902,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return child; return child;
} }
/// The node this route will use for its root [FocusScope] widget.
final FocusScopeNode focusScopeNode = FocusScopeNode();
@override @override
void install(OverlayEntry insertionPoint) { void install(OverlayEntry insertionPoint) {
super.install(insertionPoint); super.install(insertionPoint);
...@@ -899,14 +911,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -899,14 +911,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
@override @override
TickerFuture didPush() { TickerFuture didPush() {
navigator.focusScopeNode.setFirstFocus(focusScopeNode); if (_scopeKey.currentState != null) {
return super.didPush(); navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
} }
return super.didPush();
@override
void dispose() {
focusScopeNode.detach();
super.dispose();
} }
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
......
...@@ -106,7 +106,10 @@ void main() { ...@@ -106,7 +106,10 @@ void main() {
testWidgets('Last tab gets focus', (WidgetTester tester) async { testWidgets('Last tab gets focus', (WidgetTester tester) async {
// 2 nodes for 2 tabs // 2 nodes for 2 tabs
final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
];
await tester.pumpWidget( await tester.pumpWidget(
CupertinoApp( CupertinoApp(
...@@ -139,7 +142,10 @@ void main() { ...@@ -139,7 +142,10 @@ void main() {
testWidgets('Do not affect focus order in the route', (WidgetTester tester) async { testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
final List<FocusNode> focusNodes = <FocusNode>[ final List<FocusNode> focusNodes = <FocusNode>[
FocusNode(), FocusNode(), FocusNode(), FocusNode(), FocusNode(debugLabel: 'Node 1'),
FocusNode(debugLabel: 'Node 2'),
FocusNode(debugLabel: 'Node 3'),
FocusNode(debugLabel: 'Node 4'),
]; ];
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -9,11 +9,14 @@ void main() { ...@@ -9,11 +9,14 @@ void main() {
testWidgets('Dialog interaction', (WidgetTester tester) async { testWidgets('Dialog interaction', (WidgetTester tester) async {
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
final FocusNode focusNode = FocusNode(debugLabel: 'Editable Text Node');
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( MaterialApp(
home: Material( home: Material(
child: Center( child: Center(
child: TextField( child: TextField(
focusNode: focusNode,
autofocus: true, autofocus: true,
), ),
), ),
...@@ -130,7 +133,7 @@ void main() { ...@@ -130,7 +133,7 @@ void main() {
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
expect(tester.testTextInput.isVisible, isFalse); expect(tester.testTextInput.isVisible, isFalse);
}, skip: true); // https://github.com/flutter/flutter/issues/29384. });
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async { testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
......
...@@ -2754,12 +2754,12 @@ void main() { ...@@ -2754,12 +2754,12 @@ void main() {
controller = TextEditingController(); controller = TextEditingController();
}); });
MaterialApp setupWidget() { Future<void> setupWidget(WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
controller = TextEditingController(); controller = TextEditingController();
return MaterialApp( await tester.pumpWidget(
MaterialApp(
home: Material( home: Material(
child: RawKeyboardListener( child: RawKeyboardListener(
focusNode: focusNode, focusNode: focusNode,
...@@ -2769,14 +2769,15 @@ void main() { ...@@ -2769,14 +2769,15 @@ void main() {
maxLines: 3, maxLines: 3,
strutStyle: StrutStyle.disabled, strutStyle: StrutStyle.disabled,
), ),
) , ),
),
), ),
); );
await tester.pump();
} }
testWidgets('Shift test 1', (WidgetTester tester) async { testWidgets('Shift test 1', (WidgetTester tester) async {
await setupWidget(tester);
await tester.pumpWidget(setupWidget());
const String testValue = 'a big house'; const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2789,7 +2790,7 @@ void main() { ...@@ -2789,7 +2790,7 @@ void main() {
}); });
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgets('Control Shift test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'their big house'; const String testValue = 'their big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2805,7 +2806,7 @@ void main() { ...@@ -2805,7 +2806,7 @@ void main() {
}); });
testWidgets('Down and up test', (WidgetTester tester) async { testWidgets('Down and up test', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house'; const String testValue = 'a big house';
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2827,7 +2828,7 @@ void main() { ...@@ -2827,7 +2828,7 @@ void main() {
}); });
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgets('Down and up test 2', (WidgetTester tester) async {
await tester.pumpWidget(setupWidget()); await setupWidget(tester);
const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse\nOne more line yay'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2914,6 +2915,8 @@ void main() { ...@@ -2914,6 +2915,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -2984,6 +2987,8 @@ void main() { ...@@ -2984,6 +2987,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
...@@ -3093,6 +3098,8 @@ void main() { ...@@ -3093,6 +3098,8 @@ void main() {
), ),
), ),
); );
focusNode.requestFocus();
await tester.pump();
const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19 const String testValue = 'a big house\njumped over a mouse'; // 11 \n 19
await tester.enterText(find.byType(TextField), testValue); await tester.enterText(find.byType(TextField), testValue);
......
...@@ -17,8 +17,8 @@ import 'editable_text_utils.dart'; ...@@ -17,8 +17,8 @@ import 'editable_text_utils.dart';
import 'semantics_tester.dart'; import 'semantics_tester.dart';
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode(debugLabel: 'EditableText Node');
final FocusScopeNode focusScopeNode = FocusScopeNode(); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'EditableText Scope Node');
const TextStyle textStyle = TextStyle(); const TextStyle textStyle = TextStyle();
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00); const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
...@@ -955,6 +955,9 @@ void main() { ...@@ -955,6 +955,9 @@ void main() {
), ),
)); ));
focusNode.requestFocus();
await tester.pump();
expect( expect(
semantics, semantics,
includesNodeWith( includesNodeWith(
...@@ -1512,6 +1515,8 @@ void main() { ...@@ -1512,6 +1515,8 @@ void main() {
), ),
); );
focusNode.requestFocus();
// Now change it to make it obscure text. // Now change it to make it obscure text.
await tester.pumpWidget(MaterialApp( await tester.pumpWidget(MaterialApp(
home: EditableText( home: EditableText(
...@@ -1884,7 +1889,7 @@ void main() { ...@@ -1884,7 +1889,7 @@ void main() {
composing: TextRange(start: 5, end: 14), composing: TextRange(start: 5, end: 14),
), ),
); );
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode(debugLabel: 'Test Focus Node');
await tester.pumpWidget(MaterialApp( // So we can show overlays. await tester.pumpWidget(MaterialApp( // So we can show overlays.
home: EditableText( home: EditableText(
......
// 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'
));
});
});
}
...@@ -5,22 +5,25 @@ ...@@ -5,22 +5,25 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class TestFocusable extends StatefulWidget { class TestFocus extends StatefulWidget {
const TestFocusable({ const TestFocus({
Key key, Key key,
this.debugLabel,
this.name = 'a', this.name = 'a',
this.autofocus = false, this.autofocus = false,
}) : super(key: key); }) : super(key: key);
final String debugLabel;
final String name; final String name;
final bool autofocus; final bool autofocus;
@override @override
TestFocusableState createState() => TestFocusableState(); TestFocusState createState() => TestFocusState();
} }
class TestFocusableState extends State<TestFocusable> { class TestFocusState extends State<TestFocus> {
final FocusNode focusNode = FocusNode(); FocusNode focusNode = FocusNode();
FocusAttachment focusAttachment;
bool _didAutofocus = false; bool _didAutofocus = false;
@override @override
...@@ -29,9 +32,16 @@ class TestFocusableState extends State<TestFocusable> { ...@@ -29,9 +32,16 @@ class TestFocusableState extends State<TestFocusable> {
super.dispose(); super.dispose();
} }
@override
void initState() {
super.initState();
focusNode = FocusNode(debugLabel: widget.debugLabel);
focusAttachment = focusNode.attach(context);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(focusNode); focusAttachment.reparent();
if (!_didAutofocus && widget.autofocus) { if (!_didAutofocus && widget.autofocus) {
_didAutofocus = true; _didAutofocus = true;
FocusScope.of(context).autofocus(focusNode); FocusScope.of(context).autofocus(focusNode);
...@@ -54,11 +64,12 @@ class TestFocusableState extends State<TestFocusable> { ...@@ -54,11 +64,12 @@ class TestFocusableState extends State<TestFocusable> {
} }
void main() { void main() {
group(FocusScope, () {
testWidgets('Can focus', (WidgetTester tester) async { testWidgets('Can focus', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
TestFocusable(key: key, name: 'a'), TestFocus(key: key, name: 'a'),
); );
expect(key.currentState.focusNode.hasFocus, isFalse); expect(key.currentState.focusNode.hasFocus, isFalse);
...@@ -71,13 +82,13 @@ void main() { ...@@ -71,13 +82,13 @@ void main() {
}); });
testWidgets('Can unfocus', (WidgetTester tester) async { testWidgets('Can unfocus', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: keyA, name: 'a'), TestFocus(key: keyA, name: 'a'),
TestFocusable(key: keyB, name: 'b'), TestFocus(key: keyB, name: 'b'),
], ],
), ),
); );
...@@ -106,18 +117,18 @@ void main() { ...@@ -106,18 +117,18 @@ void main() {
}); });
testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyA, key: keyA,
name: 'a', name: 'a',
autofocus: true, autofocus: true,
), ),
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -154,18 +165,23 @@ void main() { ...@@ -154,18 +165,23 @@ void main() {
// This moves a focus node first into a focus scope that is added to its // This moves a focus node first into a focus scope that is added to its
// parent, and then out of that focus scope again. // parent, and then out of that focus scope again.
testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async { testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope = FocusScopeNode(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
final FocusScopeNode childFocusScope = FocusScopeNode(); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
final GlobalKey<TestFocusableState> key = GlobalKey(); final GlobalKey<TestFocusState> key = GlobalKey();
// Initially create the focus inside of the parent FocusScope. // Initially create the focus inside of the parent FocusScope.
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope, node: parentFocusScope,
autofocus: true, autofocus: true,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: key, name: 'a'), TestFocus(
key: key,
name: 'a',
debugLabel: 'Child',
),
], ],
), ),
), ),
...@@ -181,31 +197,58 @@ void main() { ...@@ -181,31 +197,58 @@ void main() {
expect(parentFocusScope, hasAGoodToStringDeep); expect(parentFocusScope, hasAGoodToStringDeep);
expect( expect(
parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info), parentFocusScope.toStringDeep(),
equalsIgnoringHashCodes('FocusScopeNode#00000\n' equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' focus: FocusNode#00000(FOCUSED)\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, hasAGoodToStringDeep);
expect( expect(
WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info), WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info),
equalsIgnoringHashCodes('FocusScopeNode#00000\n' equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' └─child 1: FocusScopeNode#00000\n' ' │ FOCUSED\n'
' focus: FocusNode#00000(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. // Add the child focus scope to the focus tree.
final FocusAttachment childAttachment = childFocusScope.attach(key.currentContext);
parentFocusScope.setFirstFocus(childFocusScope); parentFocusScope.setFirstFocus(childFocusScope);
await tester.pumpAndSettle();
expect(childFocusScope.isFirstFocus, isTrue); expect(childFocusScope.isFirstFocus, isTrue);
// Now add the child focus scope with no focus node in it to the tree. // Now add the child focus scope with no child focusable in it to the tree.
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope, node: parentFocusScope,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: key), TestFocus(
key: key,
debugLabel: 'Child',
),
FocusScope( FocusScope(
debugLabel: 'Child Scope',
node: childFocusScope, node: childFocusScope,
child: Container(), child: Container(),
), ),
...@@ -220,12 +263,17 @@ void main() { ...@@ -220,12 +263,17 @@ void main() {
// Now move the existing focus node into the child focus scope. // Now move the existing focus node into the child focus scope.
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope, node: parentFocusScope,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
debugLabel: 'Child Scope',
node: childFocusScope, node: childFocusScope,
child: TestFocusable(key: key), child: TestFocus(
key: key,
debugLabel: 'Child',
),
), ),
], ],
), ),
...@@ -234,40 +282,47 @@ void main() { ...@@ -234,40 +282,47 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isTrue); expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('A FOCUSED'), findsOneWidget); expect(find.text('a'), findsOneWidget);
// Now remove the child focus scope. // Now remove the child focus scope.
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope, node: parentFocusScope,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable(key: key), TestFocus(
key: key,
debugLabel: 'Child',
),
], ],
), ),
), ),
); );
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(key.currentState.focusNode.hasFocus, isFalse);
expect(find.text('a'), findsOneWidget);
expect(key.currentState.focusNode.hasFocus, isTrue); // Must detach the child because we had to attach it in order to call
expect(find.text('A FOCUSED'), findsOneWidget); // setFirstFocus before adding to the widget.
childAttachment.detach();
}); });
// Arguably, this isn't correct behavior, but it is what happens now. // 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 { testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -287,7 +342,7 @@ void main() { ...@@ -287,7 +342,7 @@ void main() {
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -302,21 +357,22 @@ void main() { ...@@ -302,21 +357,22 @@ void main() {
}); });
testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async { testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope Node');
final FocusScopeNode childFocusScope = FocusScopeNode(); final FocusScopeNode childFocusScope = FocusScopeNode(debugLabel: 'Child Scope Node');
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
node: childFocusScope, node: childFocusScope,
child: TestFocusable( child: TestFocus(
debugLabel: 'Child',
key: keyA, key: keyA,
name: 'a',
), ),
), ),
); );
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
expect(FocusScope.of(keyA.currentContext), equals(childFocusScope));
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -330,38 +386,41 @@ void main() { ...@@ -330,38 +386,41 @@ void main() {
node: parentFocusScope, node: parentFocusScope,
child: FocusScope( child: FocusScope(
node: childFocusScope, node: childFocusScope,
child: TestFocusable( child: TestFocus(
debugLabel: 'Child',
key: keyA, key: keyA,
name: 'a',
), ),
), ),
), ),
); );
await tester.pump(); await tester.pump();
expect(childFocusScope.isFirstFocus, isTrue); expect(childFocusScope.isFirstFocus, isTrue);
expect(keyA.currentState.focusNode.hasFocus, isFalse); // Node keeps it's focus when moved to the new scope.
expect(find.text('a'), findsOneWidget); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('A FOCUSED'), findsOneWidget);
}); });
// Arguably, this isn't correct behavior, but it is what happens now. // 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 { testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async {
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope = FocusScopeNode(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
await tester.pumpWidget( await tester.pumpWidget(
FocusScope( FocusScope(
debugLabel: 'Parent Scope',
node: parentFocusScope, node: parentFocusScope,
autofocus: true, autofocus: true,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Widget A',
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
TestFocusable( TestFocus(
debugLabel: 'Widget B',
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -371,7 +430,8 @@ void main() { ...@@ -371,7 +430,8 @@ void main() {
); );
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); final FocusScopeNode scope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(scope);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -385,7 +445,7 @@ void main() { ...@@ -385,7 +445,7 @@ void main() {
node: parentFocusScope, node: parentFocusScope,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -400,24 +460,25 @@ void main() { ...@@ -400,24 +460,25 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
// By "pinned", it means kept in the tree by a GlobalKey. testWidgets('Removing a FocusScope removes its node from the tree', (WidgetTester tester) async {
testWidgets('Removing pinned focused scope moves focus to focused widget within next FocusScope', (WidgetTester tester) async { final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyA = GlobalKey();
final GlobalKey<TestFocusableState> scopeKeyA = GlobalKey(); final GlobalKey<TestFocusState> scopeKeyB = GlobalKey();
final GlobalKey<TestFocusableState> scopeKeyB = GlobalKey(); final FocusScopeNode parentFocusScope = FocusScopeNode(debugLabel: 'Parent Scope');
final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode();
// This checks both FocusScopes that have their own nodes, as well as those
// that use external nodes.
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
children: <Widget>[ children: <Widget>[
FocusScope( FocusScope(
key: scopeKeyA, key: scopeKeyA,
node: parentFocusScope1, node: parentFocusScope,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child A',
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -426,10 +487,10 @@ void main() { ...@@ -426,10 +487,10 @@ void main() {
), ),
FocusScope( FocusScope(
key: scopeKeyB, key: scopeKeyB,
node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child B',
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -442,8 +503,10 @@ void main() { ...@@ -442,8 +503,10 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); final FocusScopeNode bScope = FocusScope.of(keyB.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(bScope);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -453,42 +516,17 @@ void main() { ...@@ -453,42 +516,17 @@ void main() {
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
// Since the FocusScope widgets are pinned with GlobalKeys, when the first await tester.pumpWidget(Container());
// 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,
),
],
),
),
],
),
);
await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isTrue); expect(WidgetsBinding.instance.focusManager.rootScope.children, isEmpty);
expect(find.text('B FOCUSED'), findsOneWidget);
}); });
// Arguably, this isn't correct behavior, but it is what happens now. // 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 { 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<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Parent Scope 1');
final FocusScopeNode parentFocusScope2 = FocusScopeNode(); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Parent Scope 2');
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
...@@ -497,7 +535,8 @@ void main() { ...@@ -497,7 +535,8 @@ void main() {
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child A',
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -508,7 +547,8 @@ void main() { ...@@ -508,7 +547,8 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child B',
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -521,8 +561,10 @@ void main() { ...@@ -521,8 +561,10 @@ void main() {
FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode);
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(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(); await tester.pumpAndSettle();
...@@ -543,7 +585,7 @@ void main() { ...@@ -543,7 +585,7 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
autofocus: true, autofocus: true,
...@@ -560,12 +602,11 @@ void main() { ...@@ -560,12 +602,11 @@ void main() {
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
// Arguably, this isn't correct behavior, but it is what happens now. testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async {
testWidgets('Moving widget from one scope to another does not retain focus', (WidgetTester tester) async {
final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final FocusScopeNode parentFocusScope1 = FocusScopeNode();
final FocusScopeNode parentFocusScope2 = FocusScopeNode(); final FocusScopeNode parentFocusScope2 = FocusScopeNode();
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
...@@ -574,7 +615,7 @@ void main() { ...@@ -574,7 +615,7 @@ void main() {
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -585,7 +626,7 @@ void main() { ...@@ -585,7 +626,7 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -597,7 +638,8 @@ void main() { ...@@ -597,7 +638,8 @@ void main() {
); );
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -613,7 +655,7 @@ void main() { ...@@ -613,7 +655,7 @@ void main() {
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -624,7 +666,7 @@ void main() { ...@@ -624,7 +666,7 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -637,18 +679,17 @@ void main() { ...@@ -637,18 +679,17 @@ void main() {
await tester.pump(); await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('a'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); expect(find.text('b'), findsOneWidget);
}); });
// Arguably, this isn't correct behavior, but it is what happens now. testWidgets('Moving FocusScopeNodes retains focus', (WidgetTester tester) async {
testWidgets('Moving FocusScopeNodes does not retain focus', (WidgetTester tester) async { final FocusScopeNode parentFocusScope1 = FocusScopeNode(debugLabel: 'Scope 1');
final FocusScopeNode parentFocusScope1 = FocusScopeNode(); final FocusScopeNode parentFocusScope2 = FocusScopeNode(debugLabel: 'Scope 2');
final FocusScopeNode parentFocusScope2 = FocusScopeNode(); final GlobalKey<TestFocusState> keyA = GlobalKey();
final GlobalKey<TestFocusableState> keyA = GlobalKey(); final GlobalKey<TestFocusState> keyB = GlobalKey();
final GlobalKey<TestFocusableState> keyB = GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
Column( Column(
...@@ -657,7 +698,8 @@ void main() { ...@@ -657,7 +698,8 @@ void main() {
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child A',
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -668,7 +710,8 @@ void main() { ...@@ -668,7 +710,8 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child B',
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -680,7 +723,8 @@ void main() { ...@@ -680,7 +723,8 @@ void main() {
); );
FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); final FocusScopeNode aScope = FocusScope.of(keyA.currentContext);
WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(aScope);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -697,7 +741,8 @@ void main() { ...@@ -697,7 +741,8 @@ void main() {
node: parentFocusScope2, node: parentFocusScope2,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child A',
key: keyA, key: keyA,
name: 'a', name: 'a',
), ),
...@@ -708,7 +753,8 @@ void main() { ...@@ -708,7 +753,8 @@ void main() {
node: parentFocusScope1, node: parentFocusScope1,
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
TestFocusable( TestFocus(
debugLabel: 'Child B',
key: keyB, key: keyB,
name: 'b', name: 'b',
), ),
...@@ -721,9 +767,196 @@ void main() { ...@@ -721,9 +767,196 @@ void main() {
await tester.pump(); await tester.pump();
expect(keyA.currentState.focusNode.hasFocus, isFalse); expect(keyA.currentState.focusNode.hasFocus, isTrue);
expect(find.text('a'), findsOneWidget); expect(find.text('A FOCUSED'), findsOneWidget);
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isFalse);
expect(find.text('b'), findsOneWidget); 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,
),
),
),
),
),
),
);
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(),
),
),
),
Focus(
key: key4,
child: Container(
child: Focus(
key: key5,
child: Container(),
),
),
),
Focus(
key: key6,
child: Column(
children: <Widget>[
Focus(
key: key7,
child: Container(),
),
Focus(
key: key8,
child: Container(),
),
],
),
),
],
),
),
);
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();
expect(rootNode.hasFocus, isTrue);
expect(rootNode, equals(firstElement.owner.focusManager.rootScope));
});
});
testWidgets('Nodes are removed when all Focuses are removed.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
bool gotFocus;
await tester.pumpWidget(
FocusScope(
child: 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);
await tester.pumpWidget(Container());
expect(WidgetsBinding.instance.focusManager.rootScope.descendants, isEmpty);
});
} }
...@@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) { ...@@ -12,7 +12,7 @@ void sendFakeKeyEvent(Map<String, dynamic> data) {
BinaryMessages.handlePlatformMessage( BinaryMessages.handlePlatformMessage(
SystemChannels.keyEvent.name, SystemChannels.keyEvent.name,
SystemChannels.keyEvent.codec.encodeMessage(data), SystemChannels.keyEvent.codec.encodeMessage(data),
(ByteData data) { }, (ByteData data) {},
); );
} }
...@@ -29,13 +29,15 @@ void main() { ...@@ -29,13 +29,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
RawKeyboardListener(
focusNode: focusNode, focusNode: focusNode,
onKey: events.add, onKey: events.add,
child: Container(), child: Container(),
)); ),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
...@@ -65,13 +67,15 @@ void main() { ...@@ -65,13 +67,15 @@ void main() {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
await tester.pumpWidget(RawKeyboardListener( await tester.pumpWidget(
RawKeyboardListener(
focusNode: focusNode, focusNode: focusNode,
onKey: events.add, onKey: events.add,
child: Container(), child: Container(),
)); ),
);
tester.binding.focusManager.rootScope.requestFocus(focusNode); focusNode.requestFocus();
await tester.idle(); await tester.idle();
sendFakeKeyEvent(<String, dynamic>{ sendFakeKeyEvent(<String, dynamic>{
......
...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase ...@@ -692,6 +692,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
FlutterError.onError = _oldExceptionHandler; FlutterError.onError = _oldExceptionHandler;
_pendingExceptionDetails = null; _pendingExceptionDetails = null;
_parentZone = null; _parentZone = null;
buildOwner.focusManager = FocusManager();
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment