| // Copyright 2014 The Flutter 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:async'; |
| import 'dart:ui'; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/services.dart'; |
| |
| import 'binding.dart'; |
| import 'focus_scope.dart'; |
| import 'focus_traversal.dart'; |
| import 'framework.dart'; |
| |
| /// Setting to true will cause extensive logging to occur when focus changes occur. |
| /// |
| /// Can be used to debug focus issues: each time the focus changes, the focus |
| /// tree will be printed and requests for focus and other focus operations will |
| /// be logged. |
| bool debugFocusChanges = false; |
| |
| bool _focusDebug(String message, [Iterable<String>? details]) { |
| if (debugFocusChanges) { |
| debugPrint('FOCUS: $message'); |
| if (details != null && details.isNotEmpty) { |
| for (final String detail in details) { |
| debugPrint(' $detail'); |
| } |
| } |
| } |
| // Return true so that it can be easily used inside of an assert. |
| return true; |
| } |
| |
| /// An enum that describes how to handle a key event handled by a |
| /// [FocusOnKeyCallback] or [FocusOnKeyEventCallback]. |
| enum KeyEventResult { |
| /// The key event has been handled, and the event should not be propagated to |
| /// other key event handlers. |
| handled, |
| /// The key event has not been handled, and the event should continue to be |
| /// propagated to other key event handlers, even non-Flutter ones. |
| ignored, |
| /// The key event has not been handled, but the key event should not be |
| /// propagated to other key event handlers. |
| /// |
| /// It will be returned to the platform embedding to be propagated to text |
| /// fields and non-Flutter key event handlers on the platform. |
| skipRemainingHandlers, |
| } |
| |
| /// Combine the results returned by multiple [FocusOnKeyCallback]s or |
| /// [FocusOnKeyEventCallback]s. |
| /// |
| /// If any callback returns [KeyEventResult.handled], the node considers the |
| /// message handled; otherwise, if any callback returns |
| /// [KeyEventResult.skipRemainingHandlers], the node skips the remaining |
| /// handlers without preventing the platform to handle; otherwise the node is |
| /// ignored. |
| KeyEventResult combineKeyEventResults(Iterable<KeyEventResult> results) { |
| bool hasSkipRemainingHandlers = false; |
| for (final KeyEventResult result in results) { |
| switch (result) { |
| case KeyEventResult.handled: |
| return KeyEventResult.handled; |
| case KeyEventResult.skipRemainingHandlers: |
| hasSkipRemainingHandlers = true; |
| break; |
| case KeyEventResult.ignored: |
| break; |
| } |
| } |
| return hasSkipRemainingHandlers ? |
| KeyEventResult.skipRemainingHandlers : |
| KeyEventResult.ignored; |
| } |
| |
| /// Signature of a callback used by [Focus.onKey] and [FocusScope.onKey] |
| /// to receive key events. |
| /// |
| /// The [node] is the node that received the event. |
| /// |
| /// Returns a [KeyEventResult] that describes how, and whether, the key event |
| /// was handled. |
| typedef FocusOnKeyCallback = KeyEventResult Function(FocusNode node, RawKeyEvent event); |
| |
| /// Signature of a callback used by [Focus.onKeyEvent] and [FocusScope.onKeyEvent] |
| /// to receive key events. |
| /// |
| /// The [node] is the node that received the event. |
| /// |
| /// Returns a [KeyEventResult] that describes how, and whether, the key event |
| /// was handled. |
| typedef FocusOnKeyEventCallback = KeyEventResult Function(FocusNode node, KeyEvent event); |
| |
| // Represents a pending autofocus request. |
| @immutable |
| class _Autofocus { |
| const _Autofocus({ required this.scope, required this.autofocusNode }); |
| |
| final FocusScopeNode scope; |
| final FocusNode autofocusNode; |
| |
| // Applies the autofocus request, if the node is still attached to the |
| // original scope and the scope has no focused child. |
| // |
| // The widget tree is responsible for calling reparent/detach on attached |
| // nodes to keep their parent/manager information up-to-date, so here we can |
| // safely check if the scope/node involved in each autofocus request is |
| // still attached, and discard the ones are no longer attached to the |
| // original manager. |
| void applyIfValid(FocusManager manager) { |
| final bool shouldApply = (scope.parent != null || identical(scope, manager.rootScope)) |
| && identical(scope._manager, manager) |
| && scope.focusedChild == null |
| && autofocusNode.ancestors.contains(scope); |
| if (shouldApply) { |
| assert(_focusDebug('Applying autofocus: $autofocusNode')); |
| autofocusNode._doRequestFocus(findFirstFocus: true); |
| } else { |
| assert(_focusDebug('Autofocus request discarded for node: $autofocusNode.')); |
| } |
| } |
| } |
| |
| /// An attachment point for a [FocusNode]. |
| /// |
| /// Using a [FocusAttachment] is rarely needed, unless you are building |
| /// something akin to the [Focus] or [FocusScope] widgets from scratch. |
| /// |
| /// Once created, a [FocusNode] must be attached to the widget tree by its |
| /// _host_ [StatefulWidget] via a [FocusAttachment] object. [FocusAttachment]s |
| /// are owned by the [StatefulWidget] that hosts a [FocusNode] or |
| /// [FocusScopeNode]. There can be multiple [FocusAttachment]s for each |
| /// [FocusNode], but the node will only ever be attached to one of them at a |
| /// time. |
| /// |
| /// This attachment is created by calling [FocusNode.attach], usually from the |
| /// host widget's [State.initState] method. If the widget is updated to have a |
| /// different focus node, then the new node needs to be attached in |
| /// [State.didUpdateWidget], after calling [detach] on the previous |
| /// [FocusAttachment]. Once detached, the attachment is defunct and will no |
| /// longer make changes to the [FocusNode] through [reparent]. |
| /// |
| /// Without these attachment points, it would be possible for a focus node to |
| /// simultaneously be attached to more than one part of the widget tree during |
| /// the build stage. |
| class FocusAttachment { |
| /// A private constructor, because [FocusAttachment]s are only to be created |
| /// by [FocusNode.attach]. |
| FocusAttachment._(this._node) : assert(_node != null); |
| |
| // The focus node that this attachment manages an attachment for. The node may |
| // not yet have a parent, or may have been detached from this attachment, so |
| // don't count on this node being in a usable state. |
| final FocusNode _node; |
| |
| /// Returns true if the associated node is attached to this attachment. |
| /// |
| /// It is possible to be attached to the widget tree, but not be placed in |
| /// the focus tree (i.e. to not have a parent yet in the focus tree). |
| bool get isAttached => _node._attachment == this; |
| |
| /// Detaches the [FocusNode] this attachment point is associated with from the |
| /// focus tree, and disconnects it from this attachment point. |
| /// |
| /// Calling [FocusNode.dispose] will also automatically detach the node. |
| void detach() { |
| assert(_node != null); |
| assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}'])); |
| if (isAttached) { |
| if (_node.hasPrimaryFocus || (_node._manager != null && _node._manager!._markedForFocus == _node)) { |
| _node.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); |
| } |
| // This node is no longer in the tree, so shouldn't send notifications anymore. |
| _node._manager?._markDetached(_node); |
| _node._parent?._removeChild(_node); |
| _node._attachment = null; |
| assert(!_node.hasPrimaryFocus); |
| assert(_node._manager?._markedForFocus != _node); |
| } |
| 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.maybeOf(_node.context!, scopeOk: true); |
| parent ??= _node.context!.owner!.focusManager.rootScope; |
| assert(parent != null); |
| parent._reparent(_node); |
| } |
| } |
| } |
| |
| /// Describe what should happen after [FocusNode.unfocus] is called. |
| /// |
| /// See also: |
| /// |
| /// * [FocusNode.unfocus], which takes this as its `disposition` parameter. |
| enum UnfocusDisposition { |
| /// Focus the nearest focusable enclosing scope of this node, but do not |
| /// descend to locate the leaf [FocusScopeNode.focusedChild] the way |
| /// [previouslyFocusedChild] does. |
| /// |
| /// Focusing the scope in this way clears the [FocusScopeNode.focusedChild] |
| /// history for the enclosing scope when it receives focus. Because of this, |
| /// calling a traversal method like [FocusNode.nextFocus] after unfocusing |
| /// will cause the [FocusTraversalPolicy] to pick the node it thinks should be |
| /// first in the scope. |
| /// |
| /// This is the default disposition for [FocusNode.unfocus]. |
| scope, |
| |
| /// Focus the previously focused child of the nearest focusable enclosing |
| /// scope of this node. |
| /// |
| /// If there is no previously focused child, then this is equivalent to |
| /// using the [scope] disposition. |
| /// |
| /// Unfocusing with this disposition will cause [FocusNode.unfocus] to walk up |
| /// the tree to the nearest focusable enclosing scope, then start to walk down |
| /// the tree, looking for a focused child at its |
| /// [FocusScopeNode.focusedChild]. |
| /// |
| /// If the [FocusScopeNode.focusedChild] is a scope, then look for its |
| /// [FocusScopeNode.focusedChild], and so on, finding the leaf |
| /// [FocusScopeNode.focusedChild] that is not a scope, or, failing that, a |
| /// leaf scope that has no focused child. |
| previouslyFocusedChild, |
| } |
| |
| /// 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, but doing |
| /// this yourself is rare._ |
| /// |
| /// [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. |
| /// |
| /// [FocusNode]s are organized into _scopes_ (see [FocusScopeNode]), which form |
| /// sub-trees of nodes that restrict traversal to a group of nodes. Within a |
| /// scope, the most recent nodes to have focus are remembered, and if a node is |
| /// focused and then unfocused, 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. [FocusNode.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.FocusNode.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 re-parented 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 [FocusAttachment.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 [FocusAttachment.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.FocusNode.keyEvents} |
| /// ## Key Event Propagation |
| /// |
| /// The [FocusManager] receives key events from [RawKeyboard] and |
| /// [HardwareKeyboard] and will pass them to the focused nodes. It starts with |
| /// the node with the primary focus, and will call the [onKey] or [onKeyEvent] |
| /// callback for that node. If the callback returns [KeyEventResult.ignored], |
| /// indicating that it did not handle the event, the [FocusManager] will move |
| /// to the parent of that node and call its [onKey] or [onKeyEvent]. If that |
| /// [onKey] or [onKeyEvent] returns [KeyEventResult.handled], then it will stop |
| /// propagating the event. If it reaches the root [FocusScopeNode], |
| /// [FocusManager.rootScope], the event is discarded. |
| /// {@endtemplate} |
| /// |
| /// ## Focus Traversal |
| /// |
| /// The term _traversal_, sometimes called _tab traversal_, refers to moving the |
| /// focus from one widget to the next in a particular order (also sometimes |
| /// referred to as the _tab order_, since the TAB key is often bound to the |
| /// action to move to the next widget). |
| /// |
| /// To give focus to the logical _next_ or _previous_ widget in the UI, call the |
| /// [nextFocus] or [previousFocus] methods. To give the focus to a widget in a |
| /// particular direction, call the [focusInDirection] method. |
| /// |
| /// The policy for what the _next_ or _previous_ widget is, or the widget in a |
| /// particular direction, is determined by the [FocusTraversalPolicy] in force. |
| /// |
| /// The ambient policy is determined by looking up the widget hierarchy for a |
| /// [FocusTraversalGroup] widget, and obtaining the focus traversal policy from |
| /// it. Different focus nodes can inherit difference policies, so part of the |
| /// app can go in a predefined order (using [OrderedTraversalPolicy]), and part |
| /// can go in reading order (using [ReadingOrderTraversalPolicy]), depending |
| /// upon the use case. |
| /// |
| /// Predefined policies include [WidgetOrderTraversalPolicy], |
| /// [ReadingOrderTraversalPolicy], [OrderedTraversalPolicy], and |
| /// [DirectionalFocusTraversalPolicyMixin], but custom policies can be built |
| /// based upon these policies. See [FocusTraversalPolicy] for more information. |
| /// |
| /// {@tool dartpad} |
| /// 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. |
| /// |
| /// ** See code in examples/api/lib/widgets/focus_manager/focus_node.0.dart ** |
| /// {@end-tool} |
| /// |
| /// See also: |
| /// |
| /// * [Focus], a widget that manages a [FocusNode] and provides access to focus |
| /// information and actions to its descendant widgets. |
| /// * [FocusTraversalGroup], a widget used to group together and configure the |
| /// focus traversal policy for a widget subtree. |
| /// * [FocusManager], a singleton that manages the primary focus and distributes |
| /// key events to focused nodes. |
| /// * [FocusTraversalPolicy], a class used to determine how to move the focus to |
| /// other nodes. |
| class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { |
| /// Creates a focus node. |
| /// |
| /// The [debugLabel] is ignored on release builds. |
| /// |
| /// The [skipTraversal], [descendantsAreFocusable], and [canRequestFocus] |
| /// arguments must not be null. |
| /// |
| /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`. |
| /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated |
| /// in the future. |
| FocusNode({ |
| String? debugLabel, |
| this.onKey, |
| this.onKeyEvent, |
| bool skipTraversal = false, |
| bool canRequestFocus = true, |
| bool descendantsAreFocusable = true, |
| bool descendantsAreTraversable = true, |
| }) : assert(skipTraversal != null), |
| assert(canRequestFocus != null), |
| assert(descendantsAreFocusable != null), |
| _skipTraversal = skipTraversal, |
| _canRequestFocus = canRequestFocus, |
| _descendantsAreFocusable = descendantsAreFocusable, |
| _descendantsAreTraversable = descendantsAreTraversable { |
| // Set it via the setter so that it does nothing on release builds. |
| this.debugLabel = debugLabel; |
| } |
| |
| /// If true, tells the focus traversal policy to skip over this node for |
| /// purposes of the traversal algorithm. |
| /// |
| /// This may be used to place nodes in the focus tree that may be focused, but |
| /// not traversed, allowing them to receive key events as part of the focus |
| /// chain, but not be traversed to via focus traversal. |
| /// |
| /// This is different from [canRequestFocus] because it only implies that the |
| /// node can't be reached via traversal, not that it can't be focused. It may |
| /// still be focused explicitly. |
| bool get skipTraversal { |
| if (_skipTraversal) { |
| return true; |
| } |
| for (final FocusNode ancestor in ancestors) { |
| if (!ancestor.descendantsAreTraversable) { |
| return true; |
| } |
| } |
| return false; |
| } |
| bool _skipTraversal; |
| set skipTraversal(bool value) { |
| if (value != _skipTraversal) { |
| _skipTraversal = value; |
| _manager?._markPropertiesChanged(this); |
| } |
| } |
| |
| /// If true, this focus node may request the primary focus. |
| /// |
| /// Defaults to true. Set to false if you want this node to do nothing when |
| /// [requestFocus] is called on it. |
| /// |
| /// If set to false on a [FocusScopeNode], will cause all of the children of |
| /// the scope node to not be focusable. |
| /// |
| /// If set to false on a [FocusNode], it will not affect the focusability of |
| /// children of the node. |
| /// |
| /// The [hasFocus] member can still return true if this node is the ancestor |
| /// of a node with primary focus. |
| /// |
| /// This is different than [skipTraversal] because [skipTraversal] still |
| /// allows the node to be focused, just not traversed to via the |
| /// [FocusTraversalPolicy]. |
| /// |
| /// Setting [canRequestFocus] to false implies that the node will also be |
| /// skipped for traversal purposes. |
| /// |
| /// See also: |
| /// |
| /// * [FocusTraversalGroup], a widget used to group together and configure the |
| /// focus traversal policy for a widget subtree. |
| /// * [FocusTraversalPolicy], a class that can be extended to describe a |
| /// traversal policy. |
| bool get canRequestFocus { |
| if (!_canRequestFocus) { |
| return false; |
| } |
| final FocusScopeNode? scope = enclosingScope; |
| if (scope != null && !scope.canRequestFocus) { |
| return false; |
| } |
| for (final FocusNode ancestor in ancestors) { |
| if (!ancestor.descendantsAreFocusable) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool _canRequestFocus; |
| @mustCallSuper |
| set canRequestFocus(bool value) { |
| if (value != _canRequestFocus) { |
| // Have to set this first before unfocusing, since it checks this to cull |
| // unfocusable, previously-focused children. |
| _canRequestFocus = value; |
| if (hasFocus && !value) { |
| unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); |
| } |
| _manager?._markPropertiesChanged(this); |
| } |
| } |
| |
| /// If false, will disable focus for all of this node's descendants. |
| /// |
| /// Defaults to true. Does not affect focusability of this node: for that, |
| /// use [canRequestFocus]. |
| /// |
| /// If any descendants are focused when this is set to false, they will be |
| /// unfocused. When [descendantsAreFocusable] is set to true again, they will |
| /// not be refocused, although they will be able to accept focus again. |
| /// |
| /// Does not affect the value of [canRequestFocus] on the descendants. |
| /// |
| /// If a descendant node loses focus when this value is changed, the focus |
| /// will move to the scope enclosing this node. |
| /// |
| /// See also: |
| /// |
| /// * [ExcludeFocus], a widget that uses this property to conditionally |
| /// exclude focus for a subtree. |
| /// * [descendantsAreTraversable], which makes this widget's descendants |
| /// untraversable. |
| /// * [ExcludeFocusTraversal], a widget that conditionally excludes focus |
| /// traversal for a subtree. |
| /// * [Focus], a widget that exposes this setting as a parameter. |
| /// * [FocusTraversalGroup], a widget used to group together and configure |
| /// the focus traversal policy for a widget subtree that also has a |
| /// `descendantsAreFocusable` parameter that prevents its children from |
| /// being focused. |
| bool get descendantsAreFocusable => _descendantsAreFocusable; |
| bool _descendantsAreFocusable; |
| @mustCallSuper |
| set descendantsAreFocusable(bool value) { |
| if (value == _descendantsAreFocusable) { |
| return; |
| } |
| // Set _descendantsAreFocusable before unfocusing, so the scope won't try |
| // and focus any of the children here again if it is false. |
| _descendantsAreFocusable = value; |
| if (!value && hasFocus) { |
| unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); |
| } |
| _manager?._markPropertiesChanged(this); |
| } |
| |
| /// If false, tells the focus traversal policy to skip over for all of this |
| /// node's descendants for purposes of the traversal algorithm. |
| /// |
| /// Defaults to true. Does not affect the focus traversal of this node: for |
| /// that, use [skipTraversal]. |
| /// |
| /// Does not affect the value of [FocusNode.skipTraversal] on the |
| /// descendants. Does not affect focusability of the descendants. |
| /// |
| /// See also: |
| /// |
| /// * [ExcludeFocusTraversal], a widget that uses this property to conditionally |
| /// exclude focus traversal for a subtree. |
| /// * [descendantsAreFocusable], which makes this widget's descendants |
| /// unfocusable. |
| /// * [ExcludeFocus], a widget that conditionally excludes focus for a subtree. |
| /// * [FocusTraversalGroup], a widget used to group together and configure |
| /// the focus traversal policy for a widget subtree that also has an |
| /// `descendantsAreFocusable` parameter that prevents its children from |
| /// being focused. |
| bool get descendantsAreTraversable => _descendantsAreTraversable; |
| bool _descendantsAreTraversable; |
| @mustCallSuper |
| set descendantsAreTraversable(bool value) { |
| if (value != _descendantsAreTraversable) { |
| _descendantsAreTraversable = value; |
| _manager?._markPropertiesChanged(this); |
| } |
| } |
| |
| /// 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). |
| /// |
| /// This is a legacy API based on [RawKeyEvent] and will be deprecated in the |
| /// future. Prefer [onKeyEvent] instead. |
| /// |
| /// {@macro flutter.widgets.FocusNode.keyEvents} |
| FocusOnKeyCallback? onKey; |
| |
| /// Called if this focus node receives a key event while focused (i.e. when |
| /// [hasFocus] returns true). |
| /// |
| /// {@macro flutter.widgets.FocusNode.keyEvents} |
| FocusOnKeyEventCallback? onKeyEvent; |
| |
| FocusManager? _manager; |
| List<FocusNode>? _ancestors; |
| List<FocusNode>? _descendants; |
| bool _hasKeyboardToken = false; |
| |
| /// 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>[]; |
| |
| /// An iterator over the children that are allowed to be traversed by the |
| /// [FocusTraversalPolicy]. |
| /// |
| /// Returns the list of focusable, traversable children of this node, |
| /// regardless of those settings on this focus node. Will return an empty |
| /// iterable if [descendantsAreFocusable] is false. |
| /// |
| /// See also |
| /// |
| /// * [traversalDescendants], which traverses all of the node's descendants, |
| /// not just the immediate children. |
| Iterable<FocusNode> get traversalChildren { |
| if (!descendantsAreFocusable) { |
| return const Iterable<FocusNode>.empty(); |
| } |
| return children.where( |
| (FocusNode node) => !node.skipTraversal && node.canRequestFocus, |
| ); |
| } |
| |
| /// 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 { |
| if (_descendants == null) { |
| final List<FocusNode> result = <FocusNode>[]; |
| for (final FocusNode child in _children) { |
| result.addAll(child.descendants); |
| result.add(child); |
| } |
| _descendants = result; |
| } |
| return _descendants!; |
| } |
| |
| /// Returns all descendants which do not have the [skipTraversal] and do have |
| /// the [canRequestFocus] flag set. |
| Iterable<FocusNode> get traversalDescendants { |
| if (!descendantsAreFocusable) { |
| return const Iterable<FocusNode>.empty(); |
| } |
| return descendants.where((FocusNode node) => !node.skipTraversal && node.canRequestFocus); |
| } |
| |
| /// 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 |
| /// [FocusScopeNode] ([FocusManager.rootScope]). |
| Iterable<FocusNode> get ancestors { |
| if (_ancestors == null) { |
| final List<FocusNode> result = <FocusNode>[]; |
| FocusNode? parent = _parent; |
| while (parent != null) { |
| result.add(parent); |
| parent = parent._parent; |
| } |
| _ancestors = result; |
| } |
| return _ancestors!; |
| } |
| |
| /// 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 => hasPrimaryFocus || (_manager?.primaryFocus?.ancestors.contains(this) ?? false); |
| |
| /// Returns true if this node currently has the application-wide input focus. |
| /// |
| /// A [FocusNode] has the primary focus when the node is focused in its |
| /// nearest ancestor [FocusScopeNode] and [hasFocus] is true for all its |
| /// ancestor nodes, but none of its descendants. |
| /// |
| /// This is different from [hasFocus] in that [hasFocus] is true if the node |
| /// is anywhere in the focus chain, but here the node has to be at the end of |
| /// the chain to return true. |
| /// |
| /// A node that returns true for [hasPrimaryFocus] will be the first node to |
| /// receive key events through its [onKey] handler. |
| /// |
| /// This object notifies its listeners whenever this value changes. |
| bool get hasPrimaryFocus => _manager?.primaryFocus == this; |
| |
| /// Returns the [FocusHighlightMode] that is currently in effect for this node. |
| FocusHighlightMode get highlightMode => FocusManager.instance.highlightMode; |
| |
| /// 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 { |
| for (final FocusNode node in ancestors) { |
| if (node is FocusScopeNode) { |
| return node; |
| } |
| } |
| return null; |
| } |
| |
| /// Returns the size of the attached widget's [RenderObject], in logical |
| /// units. |
| /// |
| /// Size is the size of the transformed widget in global coordinates. |
| Size get size => rect.size; |
| |
| /// Returns the global offset to the upper left corner of the attached |
| /// widget's [RenderObject], in logical units. |
| /// |
| /// Offset is the offset of the transformed widget in global coordinates. |
| 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. ' |
| 'Setting the context 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 is the rectangle of the transformed widget in global coordinates. |
| 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. ' |
| 'Setting the context is typically done with the attach method.', |
| ); |
| final RenderObject object = context!.findRenderObject()!; |
| final Offset topLeft = MatrixUtils.transformPoint(object.getTransformTo(null), object.semanticBounds.topLeft); |
| final Offset bottomRight = MatrixUtils.transformPoint(object.getTransformTo(null), object.semanticBounds.bottomRight); |
| return Rect.fromLTRB(topLeft.dx, topLeft.dy, bottomRight.dx, bottomRight.dy); |
| } |
| |
| /// Removes the focus on this node by moving the primary focus to another node. |
| /// |
| /// This method removes focus from a node that has the primary focus, cancels |
| /// any outstanding requests to focus it, while setting the primary focus to |
| /// another node according to the `disposition`. |
| /// |
| /// It is safe to call regardless of whether this node has ever requested |
| /// focus or not. If this node doesn't have focus or primary focus, nothing |
| /// happens. |
| /// |
| /// The `disposition` argument determines which node will receive primary |
| /// focus after this one loses it. |
| /// |
| /// If `disposition` is set to [UnfocusDisposition.scope] (the default), then |
| /// the previously focused node history of the enclosing scope will be |
| /// cleared, and the primary focus will be moved to the nearest enclosing |
| /// scope ancestor that is enabled for focus, ignoring the |
| /// [FocusScopeNode.focusedChild] for that scope. |
| /// |
| /// If `disposition` is set to [UnfocusDisposition.previouslyFocusedChild], |
| /// then this node will be removed from the previously focused list in the |
| /// [enclosingScope], and the focus will be moved to the previously focused |
| /// node of the [enclosingScope], which (if it is a scope itself), will find |
| /// its focused child, etc., until a leaf focus node is found. If there is no |
| /// previously focused child, then the scope itself will receive focus, as if |
| /// [UnfocusDisposition.scope] were specified. |
| /// |
| /// If you want this node to lose focus and the focus to move to the next or |
| /// previous node in the enclosing [FocusTraversalGroup], call [nextFocus] or |
| /// [previousFocus] instead of calling [unfocus]. |
| /// |
| /// {@tool dartpad} |
| /// This example shows the difference between the different [UnfocusDisposition] |
| /// values for [unfocus]. |
| /// |
| /// Try setting focus on the four text fields by selecting them, and then |
| /// select "UNFOCUS" to see what happens when the current |
| /// [FocusManager.primaryFocus] is unfocused. |
| /// |
| /// Try pressing the TAB key after unfocusing to see what the next widget |
| /// chosen is. |
| /// |
| /// ** See code in examples/api/lib/widgets/focus_manager/focus_node.unfocus.0.dart ** |
| /// {@end-tool} |
| void unfocus({ |
| UnfocusDisposition disposition = UnfocusDisposition.scope, |
| }) { |
| assert(disposition != null); |
| if (!hasFocus && (_manager == null || _manager!._markedForFocus != this)) { |
| return; |
| } |
| FocusScopeNode? scope = enclosingScope; |
| if (scope == null) { |
| // If the scope is null, then this is either the root node, or a node that |
| // is not yet in the tree, neither of which do anything when unfocused. |
| return; |
| } |
| switch (disposition) { |
| case UnfocusDisposition.scope: |
| // If it can't request focus, then don't modify its focused children. |
| if (scope.canRequestFocus) { |
| // Clearing the focused children here prevents re-focusing the node |
| // that we just unfocused if we immediately hit "next" after |
| // unfocusing, and also prevents choosing to refocus the next-to-last |
| // focused child if unfocus is called more than once. |
| scope._focusedChildren.clear(); |
| } |
| |
| while (!scope!.canRequestFocus) { |
| scope = scope.enclosingScope ?? _manager?.rootScope; |
| } |
| scope._doRequestFocus(findFirstFocus: false); |
| break; |
| case UnfocusDisposition.previouslyFocusedChild: |
| // Select the most recent focused child from the nearest focusable scope |
| // and focus that. If there isn't one, focus the scope itself. |
| if (scope.canRequestFocus) { |
| scope._focusedChildren.remove(this); |
| } |
| while (!scope!.canRequestFocus) { |
| scope.enclosingScope?._focusedChildren.remove(scope); |
| scope = scope.enclosingScope ?? _manager?.rootScope; |
| } |
| scope._doRequestFocus(findFirstFocus: true); |
| break; |
| } |
| assert(_focusDebug('Unfocused node:', <String>['primary focus was $this', 'next focus will be ${_manager?._markedForFocus}'])); |
| } |
| |
| /// Removes the keyboard token from this focus node if it has one. |
| /// |
| /// This mechanism helps distinguish between an input control gaining focus by |
| /// default and gaining focus as a result of an explicit user action. |
| /// |
| /// When a focus node requests the focus (either via |
| /// [FocusScopeNode.requestFocus] or [FocusScopeNode.autofocus]), the focus |
| /// node receives a keyboard token if it does not already have one. Later, |
| /// when the focus node becomes focused, the widget that manages the |
| /// [TextInputConnection] should show the keyboard (i.e. call |
| /// [TextInputConnection.show]) only if it successfully consumes the keyboard |
| /// token from the focus node. |
| /// |
| /// Returns true if this method successfully consumes the keyboard token. |
| bool consumeKeyboardToken() { |
| if (!_hasKeyboardToken) { |
| return false; |
| } |
| _hasKeyboardToken = false; |
| return true; |
| } |
| |
| // Marks the node as being the next to be focused, meaning that it will become |
| // the primary focus and notify listeners of a focus change the next time |
| // focus is resolved by the manager. If something else calls _markNextFocus |
| // before then, then that node will become the next focus instead of the |
| // previous one. |
| void _markNextFocus(FocusNode newFocus) { |
| if (_manager != null) { |
| // If we have a manager, then let it handle the focus change. |
| _manager!._markNextFocus(this); |
| return; |
| } |
| // If we don't have a manager, then change the focus locally. |
| newFocus._setAsFocusedChildForScope(); |
| newFocus._notify(); |
| if (newFocus != this) { |
| _notify(); |
| } |
| } |
| |
| // Removes the given FocusNode and its children as a child of this node. |
| @mustCallSuper |
| void _removeChild(FocusNode node, {bool removeScopeFocus = true}) { |
| assert(node != null); |
| assert(_children.contains(node), "Tried to remove a node that wasn't a child."); |
| assert(node._parent == this); |
| assert(node._manager == _manager); |
| |
| if (removeScopeFocus) { |
| node.enclosingScope?._focusedChildren.remove(node); |
| } |
| |
| node._parent = null; |
| _children.remove(node); |
| for (final FocusNode ancestor in ancestors) { |
| ancestor._descendants = null; |
| } |
| _descendants = null; |
| assert(_manager == null || !_manager!.rootScope.descendants.contains(node)); |
| } |
| |
| void _updateManager(FocusManager? manager) { |
| _manager = manager; |
| for (final FocusNode descendant in descendants) { |
| descendant._manager = manager; |
| descendant._ancestors = null; |
| } |
| } |
| |
| // Used by FocusAttachment.reparent to perform the actual parenting operation. |
| @mustCallSuper |
| void _reparent(FocusNode child) { |
| assert(child != null); |
| assert(child != this, 'Tried to make a child into a parent of itself.'); |
| if (child._parent == this) { |
| assert(_children.contains(child), "Found a node that says it's a child, but doesn't appear in the child list."); |
| // The child is already a child of this parent. |
| return; |
| } |
| assert(_manager == null || child != _manager!.rootScope, "Reparenting the root node isn't allowed."); |
| assert(!ancestors.contains(child), 'The supplied child is already an ancestor of this node. Loops are not allowed.'); |
| final FocusScopeNode? oldScope = child.enclosingScope; |
| final bool hadFocus = child.hasFocus; |
| child._parent?._removeChild(child, removeScopeFocus: oldScope != nearestScope); |
| _children.add(child); |
| child._parent = this; |
| child._ancestors = null; |
| child._updateManager(_manager); |
| for (final FocusNode ancestor in child.ancestors) { |
| ancestor._descendants = null; |
| } |
| if (hadFocus) { |
| // Update the focus chain for the current focus without changing it. |
| _manager?.primaryFocus?._setAsFocusedChildForScope(); |
| } |
| if (oldScope != null && child.context != null && child.enclosingScope != oldScope) { |
| FocusTraversalGroup.maybeOf(child.context!)?.changedScope(node: child, oldScope: oldScope); |
| } |
| if (child._requestFocusWhenReparented) { |
| child._doRequestFocus(findFirstFocus: true); |
| child._requestFocusWhenReparented = false; |
| } |
| } |
| |
| /// Called by the _host_ [StatefulWidget] to attach a [FocusNode] to the |
| /// widget tree. |
| /// |
| /// In order to attach a [FocusNode] to the widget tree, call [attach], |
| /// typically from the [StatefulWidget]'s [State.initState] method. |
| /// |
| /// If the focus node in the host widget is swapped out, the new node will |
| /// need to be attached. [FocusAttachment.detach] should be called on the old |
| /// node, and then [attach] called on the new node. This typically happens in |
| /// the [State.didUpdateWidget] method. |
| /// |
| /// To receive key events that focuses on this node, pass a listener to `onKeyEvent`. |
| /// The `onKey` is a legacy API based on [RawKeyEvent] and will be deprecated |
| /// in the future. |
| @mustCallSuper |
| FocusAttachment attach( |
| BuildContext? context, { |
| FocusOnKeyEventCallback? onKeyEvent, |
| FocusOnKeyCallback? onKey, |
| }) { |
| _context = context; |
| this.onKey = onKey ?? this.onKey; |
| this.onKeyEvent = onKeyEvent ?? this.onKeyEvent; |
| _attachment = FocusAttachment._(this); |
| return _attachment!; |
| } |
| |
| @override |
| void dispose() { |
| // Detaching will also unfocus and clean up the manager's data structures. |
| _attachment?.detach(); |
| super.dispose(); |
| } |
| |
| @mustCallSuper |
| void _notify() { |
| if (_parent == null) { |
| // no longer part of the tree, so don't notify. |
| return; |
| } |
| if (hasPrimaryFocus) { |
| _setAsFocusedChildForScope(); |
| } |
| notifyListeners(); |
| } |
| |
| /// Requests the primary focus for this node, or for a supplied [node], which |
| /// will also give focus to its [ancestors]. |
| /// |
| /// If called without a node, request focus for this node. If the node hasn't |
| /// been added to the focus tree yet, then defer the focus request until it |
| /// is, allowing newly created widgets to request focus as soon as they are |
| /// added. |
| /// |
| /// 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 [FocusScopeNode.focusedChild], then request the focus for the |
| /// focused child. This process is recursive and continues until it encounters |
| /// either a focus scope node with a null focused child or an ordinary |
| /// (non-scope) [FocusNode] is found. |
| /// |
| /// The node is notified that it has received the primary focus in a |
| /// microtask, so notification may lag the request by up to one frame. |
| void requestFocus([FocusNode? node]) { |
| if (node != null) { |
| if (node._parent == null) { |
| _reparent(node); |
| } |
| assert(node.ancestors.contains(this), 'Focus was requested for a node that is not a descendant of the scope from which it was requested.'); |
| node._doRequestFocus(findFirstFocus: true); |
| return; |
| } |
| _doRequestFocus(findFirstFocus: true); |
| } |
| |
| // Note that this is overridden in FocusScopeNode. |
| void _doRequestFocus({required bool findFirstFocus}) { |
| assert(findFirstFocus != null); |
| if (!canRequestFocus) { |
| assert(_focusDebug('Node NOT requesting focus because canRequestFocus is false: $this')); |
| return; |
| } |
| // If the node isn't part of the tree, then we just defer the focus request |
| // until the next time it is reparented, so that it's possible to focus |
| // newly added widgets. |
| if (_parent == null) { |
| _requestFocusWhenReparented = true; |
| return; |
| } |
| _setAsFocusedChildForScope(); |
| if (hasPrimaryFocus && (_manager!._markedForFocus == null || _manager!._markedForFocus == this)) { |
| return; |
| } |
| _hasKeyboardToken = true; |
| assert(_focusDebug('Node requesting focus: $this')); |
| _markNextFocus(this); |
| } |
| |
| // If set to true, the node will request focus on this node the next time |
| // this node is reparented in the focus tree. |
| // |
| // Once requestFocus has been called at the next reparenting, this value |
| // will be reset to false. |
| // |
| // This will only force a call to requestFocus for the node once the next time |
| // the node is reparented. After that, _requestFocusWhenReparented would need |
| // to be set to true again to have it be focused again on the next |
| // reparenting. |
| // |
| // This is used when requestFocus is called and there is no parent yet. |
| bool _requestFocusWhenReparented = false; |
| |
| /// Sets this node as the [FocusScopeNode.focusedChild] of the enclosing |
| /// scope. |
| /// |
| /// Sets this node as the focused child for the enclosing scope, and that |
| /// scope as the focused child for the scope above it, etc., until it reaches |
| /// the root node. It doesn't change the primary focus, it just changes what |
| /// node would be focused if the enclosing scope receives focus, and keeps |
| /// track of previously focused children in that scope, so that if the focused |
| /// child in that scope is removed, the previous focus returns. |
| void _setAsFocusedChildForScope() { |
| FocusNode scopeFocus = this; |
| for (final FocusScopeNode ancestor in ancestors.whereType<FocusScopeNode>()) { |
| assert(scopeFocus != ancestor, 'Somehow made a loop by setting focusedChild to its scope.'); |
| assert(_focusDebug('Setting $scopeFocus as focused child for scope:', <String>[ancestor.toString()])); |
| // 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; |
| } |
| } |
| |
| /// Request to move the focus to the next focus node, by calling the |
| /// [FocusTraversalPolicy.next] method. |
| /// |
| /// Returns true if it successfully found a node and requested focus. |
| bool nextFocus() => FocusTraversalGroup.of(context!).next(this); |
| |
| /// Request to move the focus to the previous focus node, by calling the |
| /// [FocusTraversalPolicy.previous] method. |
| /// |
| /// Returns true if it successfully found a node and requested focus. |
| bool previousFocus() => FocusTraversalGroup.of(context!).previous(this); |
| |
| /// Request to move the focus to the nearest focus node in the given |
| /// direction, by calling the [FocusTraversalPolicy.inDirection] method. |
| /// |
| /// Returns true if it successfully found a node and requested focus. |
| bool focusInDirection(TraversalDirection direction) => FocusTraversalGroup.of(context!).inDirection(this, direction); |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<BuildContext>('context', context, defaultValue: null)); |
| properties.add(FlagProperty('descendantsAreFocusable', value: descendantsAreFocusable, ifFalse: 'DESCENDANTS UNFOCUSABLE', defaultValue: true)); |
| properties.add(FlagProperty('descendantsAreTraversable', value: descendantsAreTraversable, ifFalse: 'DESCENDANTS UNTRAVERSABLE', defaultValue: true)); |
| properties.add(FlagProperty('canRequestFocus', value: canRequestFocus, ifFalse: 'NOT FOCUSABLE', defaultValue: true)); |
| properties.add(FlagProperty('hasFocus', value: hasFocus && !hasPrimaryFocus, ifTrue: 'IN FOCUS PATH', defaultValue: false)); |
| properties.add(FlagProperty('hasPrimaryFocus', value: hasPrimaryFocus, ifTrue: 'PRIMARY FOCUS', defaultValue: false)); |
| } |
| |
| @override |
| List<DiagnosticsNode> debugDescribeChildren() { |
| int count = 1; |
| return _children.map<DiagnosticsNode>((FocusNode child) { |
| return child.toDiagnosticsNode(name: 'Child ${count++}'); |
| }).toList(); |
| } |
| |
| @override |
| String toStringShort() { |
| final bool hasDebugLabel = debugLabel != null && debugLabel!.isNotEmpty; |
| final String extraData = '${hasDebugLabel ? debugLabel : ''}' |
| '${hasFocus && hasDebugLabel ? ' ' : ''}' |
| '${hasFocus && !hasPrimaryFocus ? '[IN FOCUS PATH]' : ''}' |
| '${hasPrimaryFocus ? '[PRIMARY FOCUS]' : ''}'; |
| return '${describeIdentity(this)}${extraData.isNotEmpty ? '($extraData)' : ''}'; |
| } |
| } |
| |
| /// A subclass of [FocusNode] that acts as a scope for its descendants, |
| /// maintaining information about which descendant is currently or was last |
| /// focused. |
| /// |
| /// _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 [FocusNode]s 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.FocusNode.lifecycle} |
| /// {@macro flutter.widgets.FocusNode.keyEvents} |
| /// |
| /// See also: |
| /// |
| /// * [Focus], a widget that manages a [FocusNode] and provides access to focus |
| /// information and actions to its descendant widgets. |
| /// * [FocusManager], a singleton that manages the primary focus and |
| /// distributes key events to focused nodes. |
| class FocusScopeNode extends FocusNode { |
| /// Creates a [FocusScopeNode]. |
| /// |
| /// All parameters are optional. |
| FocusScopeNode({ |
| super.debugLabel, |
| super.onKeyEvent, |
| super.onKey, |
| super.skipTraversal, |
| super.canRequestFocus, |
| }) : assert(skipTraversal != null), |
| assert(canRequestFocus != null), |
| super( |
| descendantsAreFocusable: true, |
| ); |
| |
| @override |
| FocusScopeNode get nearestScope => this; |
| |
| /// Returns true if this scope is the focused child of its parent scope. |
| bool get isFirstFocus => enclosingScope!.focusedChild == this; |
| |
| /// Returns the child of this node that should receive focus if this scope |
| /// node receives focus. |
| /// |
| /// If [hasFocus] is true, then this points to the child of this node that is |
| /// currently focused. |
| /// |
| /// Returns null if there is no currently focused child. |
| FocusNode? get focusedChild { |
| assert(_focusedChildren.isEmpty || _focusedChildren.last.enclosingScope == this, 'Focused child does not have the same idea of its enclosing scope as the scope does.'); |
| return _focusedChildren.isNotEmpty ? _focusedChildren.last : null; |
| } |
| |
| // 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>[]; |
| |
| /// An iterator over the children that are allowed to be traversed by the |
| /// [FocusTraversalPolicy]. |
| /// |
| /// Will return an empty iterable if this scope node is not focusable, or if |
| /// [descendantsAreFocusable] is false. |
| /// |
| /// See also: |
| /// |
| /// * [traversalDescendants], which traverses all of the node's descendants, |
| /// not just the immediate children. |
| @override |
| Iterable<FocusNode> get traversalChildren { |
| if (!canRequestFocus) { |
| return const Iterable<FocusNode>.empty(); |
| } |
| return super.traversalChildren; |
| } |
| |
| /// Returns all descendants which do not have the [skipTraversal] and do have |
| /// the [canRequestFocus] flag set. |
| /// |
| /// Will return an empty iterable if this scope node is not focusable, or if |
| /// [descendantsAreFocusable] is false. |
| @override |
| Iterable<FocusNode> get traversalDescendants { |
| if (!canRequestFocus) { |
| return const Iterable<FocusNode>.empty(); |
| } |
| return super.traversalDescendants; |
| } |
| |
| /// Make the given [scope] the active child scope for this scope. |
| /// |
| /// If the given [scope] is not yet a part of the focus tree, then add it to |
| /// the tree as a child of this scope. If it is already part of the focus |
| /// tree, the given scope must be a descendant of this scope. |
| void setFirstFocus(FocusScopeNode scope) { |
| assert(scope != null); |
| assert(scope != this, 'Unexpected self-reference in setFirstFocus.'); |
| assert(_focusDebug('Setting scope as first focus in $this to node:', <String>[scope.toString()])); |
| if (scope._parent == null) { |
| _reparent(scope); |
| } |
| assert(scope.ancestors.contains(this), '$FocusScopeNode $scope must be a child of $this to set it as first focus.'); |
| if (hasFocus) { |
| scope._doRequestFocus(findFirstFocus: true); |
| } else { |
| scope._setAsFocusedChildForScope(); |
| } |
| } |
| |
| /// If this scope lacks a focus, request that the given node become the focus. |
| /// |
| /// If the given node is not yet part of the focus tree, then add it as a |
| /// child of this node. |
| /// |
| /// Useful for widgets that wish to grab the focus if no other widget already |
| /// has the focus. |
| /// |
| /// The node is notified that it has received the primary focus in a |
| /// microtask, so notification may lag the request by up to one frame. |
| void autofocus(FocusNode node) { |
| // Attach the node to the tree first, so in _applyFocusChange if the node |
| // is detached we don't add it back to the tree. |
| if (node._parent == null) { |
| _reparent(node); |
| } |
| |
| assert(_manager != null); |
| assert(_focusDebug('Autofocus scheduled for $node: scope $this')); |
| _manager?._pendingAutofocuses.add(_Autofocus(scope: this, autofocusNode: node)); |
| _manager?._markNeedsUpdate(); |
| } |
| |
| @override |
| void _doRequestFocus({required bool findFirstFocus}) { |
| assert(findFirstFocus != null); |
| |
| // It is possible that a previously focused child is no longer focusable. |
| while (this.focusedChild != null && !this.focusedChild!.canRequestFocus) { |
| _focusedChildren.removeLast(); |
| } |
| |
| final FocusNode? focusedChild = this.focusedChild; |
| // If findFirstFocus is false, then the request is to make this scope the |
| // focus instead of looking for the ultimate first focus for this scope and |
| // its descendants. |
| if (!findFirstFocus || focusedChild == null) { |
| if (canRequestFocus) { |
| _setAsFocusedChildForScope(); |
| _markNextFocus(this); |
| } |
| return; |
| } |
| |
| focusedChild._doRequestFocus(findFirstFocus: true); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| if (_focusedChildren.isEmpty) { |
| return; |
| } |
| final List<String> childList = _focusedChildren.reversed.map<String>((FocusNode child) { |
| return child.toStringShort(); |
| }).toList(); |
| properties.add(IterableProperty<String>('focusedChildren', childList, defaultValue: const Iterable<String>.empty())); |
| } |
| } |
| |
| /// An enum to describe which kind of focus highlight behavior to use when |
| /// displaying focus information. |
| enum FocusHighlightMode { |
| /// Touch interfaces will not show the focus highlight except for controls |
| /// which bring up the soft keyboard. |
| /// |
| /// If a device that uses a traditional mouse and keyboard has a touch screen |
| /// attached, it can also enter `touch` mode if the user is using the touch |
| /// screen. |
| touch, |
| |
| /// Traditional interfaces (keyboard and mouse) will show the currently |
| /// focused control via a focus highlight of some sort. |
| /// |
| /// If a touch device (like a mobile phone) has a keyboard and/or mouse |
| /// attached, it also can enter `traditional` mode if the user is using these |
| /// input devices. |
| traditional, |
| } |
| |
| /// An enum to describe how the current value of [FocusManager.highlightMode] is |
| /// determined. The strategy is set on [FocusManager.highlightStrategy]. |
| enum FocusHighlightStrategy { |
| /// Automatic switches between the various highlight modes based on the last |
| /// kind of input that was received. This is the default. |
| automatic, |
| |
| /// [FocusManager.highlightMode] always returns [FocusHighlightMode.touch]. |
| alwaysTouch, |
| |
| /// [FocusManager.highlightMode] always returns [FocusHighlightMode.traditional]. |
| alwaysTraditional, |
| } |
| |
| /// Manages the focus tree. |
| /// |
| /// The focus tree is a separate, sparser, tree from the widget tree that |
| /// maintains the hierarchical relationship between focusable widgets in the |
| /// widget tree. |
| /// |
| /// The focus manager is responsible for tracking which [FocusNode] has the |
| /// primary input focus (the [primaryFocus]), holding the [FocusScopeNode] that |
| /// is the root of the focus tree (the [rootScope]), and what the current |
| /// [highlightMode] is. It also distributes key events from [RawKeyboard] to the |
| /// nodes in the focus tree. |
| /// |
| /// The singleton [FocusManager] instance is held by the [WidgetsBinding] as |
| /// [WidgetsBinding.focusManager], and can be conveniently accessed using the |
| /// [FocusManager.instance] static accessor. |
| /// |
| /// To find the [FocusNode] for a given [BuildContext], use [Focus.of]. To find |
| /// the [FocusScopeNode] for a given [BuildContext], use [FocusScope.of]. |
| /// |
| /// If you would like notification whenever the [primaryFocus] changes, register |
| /// a listener with [addListener]. When you no longer want to receive these |
| /// events, as when your object is about to be disposed, you must unregister |
| /// with [removeListener] to avoid memory leaks. Removing listeners is typically |
| /// done in [State.dispose] on stateful widgets. |
| /// |
| /// The [highlightMode] describes how focus highlights should be displayed on |
| /// components in the UI. The [highlightMode] changes are notified separately |
| /// via [addHighlightModeListener] and removed with |
| /// [removeHighlightModeListener]. The highlight mode changes when the user |
| /// switches from a mouse to a touch interface, or vice versa. |
| /// |
| /// The widgets that are used to manage focus in the widget tree are: |
| /// |
| /// * [Focus], a widget that manages a [FocusNode] in the focus tree so that |
| /// the focus tree reflects changes in the widget hierarchy. |
| /// * [FocusScope], a widget that manages a [FocusScopeNode] in the focus tree, |
| /// creating a new scope for restricting focus to a set of focus nodes. |
| /// * [FocusTraversalGroup], a widget that groups together nodes that should be |
| /// traversed using an order described by a given [FocusTraversalPolicy]. |
| /// |
| /// See also: |
| /// |
| /// * [FocusNode], which is a node in the focus tree that can receive focus. |
| /// * [FocusScopeNode], which is a node in the focus tree used to collect |
| /// subtrees into groups and restrict focus to them. |
| /// * The [primaryFocus] global accessor, for convenient access from anywhere |
| /// to the current focus manager state. |
| class FocusManager with DiagnosticableTreeMixin, ChangeNotifier { |
| /// Creates an object that manages the focus tree. |
| /// |
| /// This constructor is rarely called directly. To access the [FocusManager], |
| /// consider using the [FocusManager.instance] accessor instead (which gets it |
| /// from the [WidgetsBinding] singleton). |
| /// |
| /// This newly constructed focus manager does not have the necessary event |
| /// handlers registered to allow it to manage focus. To register those event |
| /// handlers, callers must call [registerGlobalHandlers]. See the |
| /// documentation in that method for caveats to watch out for. |
| FocusManager() { |
| rootScope._manager = this; |
| } |
| |
| /// Registers global input event handlers that are needed to manage focus. |
| /// |
| /// This sets the [RawKeyboard.keyEventHandler] for the shared instance of |
| /// [RawKeyboard] and adds a route to the global entry in the gesture routing |
| /// table. As such, only one [FocusManager] instance should register its |
| /// global handlers. |
| /// |
| /// When this focus manager is no longer needed, calling [dispose] on it will |
| /// unregister these handlers. |
| void registerGlobalHandlers() { |
| assert(ServicesBinding.instance.keyEventManager.keyMessageHandler == null); |
| ServicesBinding.instance.keyEventManager.keyMessageHandler = _handleKeyMessage; |
| GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent); |
| } |
| |
| @override |
| void dispose() { |
| if (ServicesBinding.instance.keyEventManager.keyMessageHandler == _handleKeyMessage) { |
| GestureBinding.instance.pointerRouter.removeGlobalRoute(_handlePointerEvent); |
| } |
| super.dispose(); |
| } |
| |
| /// Provides convenient access to the current [FocusManager] singleton from |
| /// the [WidgetsBinding] instance. |
| static FocusManager get instance => WidgetsBinding.instance.focusManager; |
| |
| /// Sets the strategy by which [highlightMode] is determined. |
| /// |
| /// If set to [FocusHighlightStrategy.automatic], then the highlight mode will |
| /// change depending upon the interaction mode used last. For instance, if the |
| /// last interaction was a touch interaction, then [highlightMode] will return |
| /// [FocusHighlightMode.touch], and focus highlights will only appear on |
| /// widgets that bring up a soft keyboard. If the last interaction was a |
| /// non-touch interaction (hardware keyboard press, mouse click, etc.), then |
| /// [highlightMode] will return [FocusHighlightMode.traditional], and focus |
| /// highlights will appear on all widgets. |
| /// |
| /// If set to [FocusHighlightStrategy.alwaysTouch] or |
| /// [FocusHighlightStrategy.alwaysTraditional], then [highlightMode] will |
| /// always return [FocusHighlightMode.touch] or |
| /// [FocusHighlightMode.traditional], respectively, regardless of the last UI |
| /// interaction type. |
| /// |
| /// The initial value of [highlightMode] depends upon the value of |
| /// [defaultTargetPlatform] and [MouseTracker.mouseIsConnected] of |
| /// [RendererBinding.mouseTracker], making a guess about which interaction is |
| /// most appropriate for the initial interaction mode. |
| /// |
| /// Defaults to [FocusHighlightStrategy.automatic]. |
| FocusHighlightStrategy get highlightStrategy => _highlightStrategy; |
| FocusHighlightStrategy _highlightStrategy = FocusHighlightStrategy.automatic; |
| set highlightStrategy(FocusHighlightStrategy highlightStrategy) { |
| _highlightStrategy = highlightStrategy; |
| _updateHighlightMode(); |
| } |
| |
| static FocusHighlightMode get _defaultModeForPlatform { |
| // Assume that if we're on one of the mobile platforms, and there's no mouse |
| // connected, that the initial interaction will be touch-based, and that |
| // it's traditional mouse and keyboard on all other platforms. |
| // |
| // This only affects the initial value: the ongoing value is updated to a |
| // known correct value as soon as any pointer/keyboard events are received. |
| switch (defaultTargetPlatform) { |
| case TargetPlatform.android: |
| case TargetPlatform.fuchsia: |
| case TargetPlatform.iOS: |
| if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) { |
| return FocusHighlightMode.traditional; |
| } |
| return FocusHighlightMode.touch; |
| case TargetPlatform.linux: |
| case TargetPlatform.macOS: |
| case TargetPlatform.windows: |
| return FocusHighlightMode.traditional; |
| } |
| } |
| |
| /// Indicates the current interaction mode for focus highlights. |
| /// |
| /// The value returned depends upon the [highlightStrategy] used, and possibly |
| /// (depending on the value of [highlightStrategy]) the most recent |
| /// interaction mode that they user used. |
| /// |
| /// If [highlightMode] returns [FocusHighlightMode.touch], then widgets should |
| /// not draw their focus highlight unless they perform text entry. |
| /// |
| /// If [highlightMode] returns [FocusHighlightMode.traditional], then widgets should |
| /// draw their focus highlight whenever they are focused. |
| // Don't want to set _highlightMode here, since it's possible for the target |
| // platform to change (especially in tests). |
| FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform; |
| FocusHighlightMode? _highlightMode; |
| |
| // If set, indicates if the last interaction detected was touch or not. |
| // If null, no interactions have occurred yet. |
| bool? _lastInteractionWasTouch; |
| |
| // Update function to be called whenever the state relating to highlightMode |
| // changes. |
| void _updateHighlightMode() { |
| final FocusHighlightMode newMode; |
| switch (highlightStrategy) { |
| case FocusHighlightStrategy.automatic: |
| if (_lastInteractionWasTouch == null) { |
| // If we don't have any information about the last interaction yet, |
| // then just rely on the default value for the platform, which will be |
| // determined based on the target platform if _highlightMode is not |
| // set. |
| return; |
| } |
| if (_lastInteractionWasTouch!) { |
| newMode = FocusHighlightMode.touch; |
| } else { |
| newMode = FocusHighlightMode.traditional; |
| } |
| break; |
| case FocusHighlightStrategy.alwaysTouch: |
| newMode = FocusHighlightMode.touch; |
| break; |
| case FocusHighlightStrategy.alwaysTraditional: |
| newMode = FocusHighlightMode.traditional; |
| break; |
| } |
| // We can't just compare newMode with _highlightMode here, since |
| // _highlightMode could be null, so we want to compare with the return value |
| // for the getter, since that's what clients will be looking at. |
| final FocusHighlightMode oldMode = highlightMode; |
| _highlightMode = newMode; |
| if (highlightMode != oldMode) { |
| _notifyHighlightModeListeners(); |
| } |
| } |
| |
| // The list of listeners for [highlightMode] state changes. |
| final HashedObserverList<ValueChanged<FocusHighlightMode>> _listeners = HashedObserverList<ValueChanged<FocusHighlightMode>>(); |
| |
| /// Register a closure to be called when the [FocusManager] notifies its listeners |
| /// that the value of [highlightMode] has changed. |
| void addHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.add(listener); |
| |
| /// Remove a previously registered closure from the list of closures that the |
| /// [FocusManager] notifies. |
| void removeHighlightModeListener(ValueChanged<FocusHighlightMode> listener) => _listeners.remove(listener); |
| |
| @pragma('vm:notify-debugger-on-exception') |
| void _notifyHighlightModeListeners() { |
| if (_listeners.isEmpty) { |
| return; |
| } |
| final List<ValueChanged<FocusHighlightMode>> localListeners = List<ValueChanged<FocusHighlightMode>>.of(_listeners); |
| for (final ValueChanged<FocusHighlightMode> listener in localListeners) { |
| try { |
| if (_listeners.contains(listener)) { |
| listener(highlightMode); |
| } |
| } catch (exception, stack) { |
| InformationCollector? collector; |
| assert(() { |
| collector = () => <DiagnosticsNode>[ |
| DiagnosticsProperty<FocusManager>( |
| 'The $runtimeType sending notification was', |
| this, |
| style: DiagnosticsTreeStyle.errorProperty, |
| ), |
| ]; |
| return true; |
| }()); |
| FlutterError.reportError(FlutterErrorDetails( |
| exception: exception, |
| stack: stack, |
| library: 'widgets library', |
| context: ErrorDescription('while dispatching notifications for $runtimeType'), |
| informationCollector: collector, |
| )); |
| } |
| } |
| } |
| |
| /// The root [FocusScopeNode] in the focus tree. |
| /// |
| /// This field is rarely used directly. To find the nearest [FocusScopeNode] |
| /// for a given [FocusNode], call [FocusNode.nearestScope]. |
| final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope'); |
| |
| void _handlePointerEvent(PointerEvent event) { |
| final FocusHighlightMode expectedMode; |
| switch (event.kind) { |
| case PointerDeviceKind.touch: |
| case PointerDeviceKind.stylus: |
| case PointerDeviceKind.invertedStylus: |
| _lastInteractionWasTouch = true; |
| expectedMode = FocusHighlightMode.touch; |
| break; |
| case PointerDeviceKind.mouse: |
| case PointerDeviceKind.trackpad: |
| case PointerDeviceKind.unknown: |
| _lastInteractionWasTouch = false; |
| expectedMode = FocusHighlightMode.traditional; |
| break; |
| } |
| if (expectedMode != highlightMode) { |
| _updateHighlightMode(); |
| } |
| } |
| |
| bool _handleKeyMessage(KeyMessage message) { |
| // Update highlightMode first, since things responding to the keys might |
| // look at the highlight mode, and it should be accurate. |
| _lastInteractionWasTouch = false; |
| _updateHighlightMode(); |
| |
| assert(_focusDebug('Received key event $message')); |
| if (_primaryFocus == null) { |
| assert(_focusDebug('No primary focus for key event, ignored: $message')); |
| return false; |
| } |
| |
| // 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 or want to |
| // stop propagation, stop. |
| bool handled = false; |
| for (final FocusNode node in <FocusNode>[_primaryFocus!, ..._primaryFocus!.ancestors]) { |
| final List<KeyEventResult> results = <KeyEventResult>[]; |
| if (node.onKeyEvent != null) { |
| for (final KeyEvent event in message.events) { |
| results.add(node.onKeyEvent!(node, event)); |
| } |
| } |
| if (node.onKey != null && message.rawEvent != null) { |
| results.add(node.onKey!(node, message.rawEvent!)); |
| } |
| final KeyEventResult result = combineKeyEventResults(results); |
| switch (result) { |
| case KeyEventResult.ignored: |
| continue; |
| case KeyEventResult.handled: |
| assert(_focusDebug('Node $node handled key event $message.')); |
| handled = true; |
| break; |
| case KeyEventResult.skipRemainingHandlers: |
| assert(_focusDebug('Node $node stopped key event propagation: $message.')); |
| handled = false; |
| break; |
| } |
| // Only KeyEventResult.ignored will continue the for loop. All other |
| // options will stop the event propagation. |
| assert(result != KeyEventResult.ignored); |
| break; |
| } |
| if (!handled) { |
| assert(_focusDebug('Key event not handled by anyone: $message.')); |
| } |
| return handled; |
| } |
| |
| /// The node that currently has the primary focus. |
| FocusNode? get primaryFocus => _primaryFocus; |
| FocusNode? _primaryFocus; |
| |
| // The set of nodes that need to notify their listeners of changes at the next |
| // update. |
| final Set<FocusNode> _dirtyNodes = <FocusNode>{}; |
| |
| // The node that has requested to have the primary focus, but hasn't been |
| // given it yet. |
| FocusNode? _markedForFocus; |
| |
| void _markDetached(FocusNode node) { |
| // The node has been removed from the tree, so it no longer needs to be |
| // notified of changes. |
| assert(_focusDebug('Node was detached: $node')); |
| if (_primaryFocus == node) { |
| _primaryFocus = null; |
| } |
| _dirtyNodes.remove(node); |
| } |
| |
| void _markPropertiesChanged(FocusNode node) { |
| _markNeedsUpdate(); |
| assert(_focusDebug('Properties changed for node $node.')); |
| _dirtyNodes.add(node); |
| } |
| |
| void _markNextFocus(FocusNode node) { |
| if (_primaryFocus == node) { |
| // The caller asked for the current focus to be the next focus, so just |
| // pretend that didn't happen. |
| _markedForFocus = null; |
| } else { |
| _markedForFocus = node; |
| _markNeedsUpdate(); |
| } |
| } |
| |
| // The list of autofocus requests made since the last _applyFocusChange call. |
| final List<_Autofocus> _pendingAutofocuses = <_Autofocus>[]; |
| |
| // True indicates that there is an update pending. |
| bool _haveScheduledUpdate = false; |
| |
| // Request that an update be scheduled, optionally requesting focus for the |
| // given newFocus node. |
| void _markNeedsUpdate() { |
| assert(_focusDebug('Scheduling update, current focus is $_primaryFocus, next focus will be $_markedForFocus')); |
| if (_haveScheduledUpdate) { |
| return; |
| } |
| _haveScheduledUpdate = true; |
| scheduleMicrotask(_applyFocusChange); |
| } |
| |
| void _applyFocusChange() { |
| _haveScheduledUpdate = false; |
| final FocusNode? previousFocus = _primaryFocus; |
| |
| for (final _Autofocus autofocus in _pendingAutofocuses) { |
| autofocus.applyIfValid(this); |
| } |
| _pendingAutofocuses.clear(); |
| |
| if (_primaryFocus == null && _markedForFocus == null) { |
| // If we don't have any current focus, and nobody has asked to focus yet, |
| // then revert to the root scope. |
| _markedForFocus = rootScope; |
| } |
| assert(_focusDebug('Refreshing focus state. Next focus will be $_markedForFocus')); |
| // A node has requested to be the next focus, and isn't already the primary |
| // focus. |
| if (_markedForFocus != null && _markedForFocus != _primaryFocus) { |
| final Set<FocusNode> previousPath = previousFocus?.ancestors.toSet() ?? <FocusNode>{}; |
| final Set<FocusNode> nextPath = _markedForFocus!.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)); |
| |
| _primaryFocus = _markedForFocus; |
| _markedForFocus = null; |
| } |
| assert(_markedForFocus == null); |
| if (previousFocus != _primaryFocus) { |
| assert(_focusDebug('Updating focus from $previousFocus to $_primaryFocus')); |
| if (previousFocus != null) { |
| _dirtyNodes.add(previousFocus); |
| } |
| if (_primaryFocus != null) { |
| _dirtyNodes.add(_primaryFocus!); |
| } |
| } |
| for (final FocusNode node in _dirtyNodes) { |
| node._notify(); |
| } |
| assert(_focusDebug('Notified ${_dirtyNodes.length} dirty nodes:', _dirtyNodes.toList().map<String>((FocusNode node) => node.toString()))); |
| _dirtyNodes.clear(); |
| if (previousFocus != _primaryFocus) { |
| notifyListeners(); |
| } |
| assert(() { |
| if (debugFocusChanges) { |
| debugDumpFocusTree(); |
| } |
| return true; |
| }()); |
| } |
| |
| @override |
| List<DiagnosticsNode> debugDescribeChildren() { |
| return <DiagnosticsNode>[ |
| rootScope.toDiagnosticsNode(name: 'rootScope'), |
| ]; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED')); |
| properties.add(DiagnosticsProperty<FocusNode>('primaryFocus', primaryFocus, defaultValue: null)); |
| properties.add(DiagnosticsProperty<FocusNode>('nextFocus', _markedForFocus, defaultValue: null)); |
| final Element? element = primaryFocus?.context as Element?; |
| if (element != null) { |
| properties.add(DiagnosticsProperty<String>('primaryFocusCreator', element.debugGetCreatorChain(20))); |
| } |
| } |
| } |
| |
| /// Provides convenient access to the current [FocusManager.primaryFocus] from the |
| /// [WidgetsBinding] instance. |
| FocusNode? get primaryFocus => WidgetsBinding.instance.focusManager.primaryFocus; |
| |
| /// 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 = FocusManager.instance.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; |
| }()); |
| } |