1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | |
7 | import 'actions.dart'; |
8 | import 'basic.dart'; |
9 | import 'focus_manager.dart'; |
10 | import 'focus_scope.dart'; |
11 | import 'framework.dart'; |
12 | import 'scroll_position.dart'; |
13 | import 'scrollable.dart'; |
14 | |
15 | // Examples can assume: |
16 | // late BuildContext context; |
17 | // FocusNode focusNode = FocusNode(); |
18 | |
19 | // BuildContext/Element doesn't have a parent accessor, but it can be simulated |
20 | // with visitAncestorElements. _getAncestor is needed because |
21 | // context.getElementForInheritedWidgetOfExactType will return itself if it |
22 | // happens to be of the correct type. _getAncestor should be O(count), since we |
23 | // always return false at a specific ancestor. By default it returns the parent, |
24 | // which is O(1). |
25 | BuildContext? _getAncestor(BuildContext context, {int count = 1}) { |
26 | BuildContext? target; |
27 | context.visitAncestorElements((Element ancestor) { |
28 | count--; |
29 | if (count == 0) { |
30 | target = ancestor; |
31 | return false; |
32 | } |
33 | return true; |
34 | }); |
35 | return target; |
36 | } |
37 | |
38 | /// Signature for the callback that's called when a traversal policy |
39 | /// requests focus. |
40 | typedef TraversalRequestFocusCallback = void Function( |
41 | FocusNode node, { |
42 | ScrollPositionAlignmentPolicy? alignmentPolicy, |
43 | double? alignment, |
44 | Duration? duration, |
45 | Curve? curve, |
46 | }); |
47 | |
48 | // A class to temporarily hold information about FocusTraversalGroups when |
49 | // sorting their contents. |
50 | class _FocusTraversalGroupInfo { |
51 | _FocusTraversalGroupInfo( |
52 | _FocusTraversalGroupNode? group, { |
53 | FocusTraversalPolicy? defaultPolicy, |
54 | List<FocusNode>? members, |
55 | }) : groupNode = group, |
56 | policy = group?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(), |
57 | members = members ?? <FocusNode>[]; |
58 | |
59 | final FocusNode? groupNode; |
60 | final FocusTraversalPolicy policy; |
61 | final List<FocusNode> members; |
62 | } |
63 | |
64 | /// A direction along either the horizontal or vertical axes. |
65 | /// |
66 | /// This is used by the [DirectionalFocusTraversalPolicyMixin], and |
67 | /// [FocusNode.focusInDirection] to indicate which direction to look in for the |
68 | /// next focus. |
69 | enum TraversalDirection { |
70 | /// Indicates a direction above the currently focused widget. |
71 | up, |
72 | |
73 | /// Indicates a direction to the right of the currently focused widget. |
74 | /// |
75 | /// This direction is unaffected by the [Directionality] of the current |
76 | /// context. |
77 | right, |
78 | |
79 | /// Indicates a direction below the currently focused widget. |
80 | down, |
81 | |
82 | /// Indicates a direction to the left of the currently focused widget. |
83 | /// |
84 | /// This direction is unaffected by the [Directionality] of the current |
85 | /// context. |
86 | left, |
87 | } |
88 | |
89 | /// Controls the transfer of focus beyond the first and the last items of a |
90 | /// [FocusScopeNode]. |
91 | /// |
92 | /// This enumeration only controls the traversal behavior performed by |
93 | /// [FocusTraversalPolicy]. Other methods of focus transfer, such as direct |
94 | /// calls to [FocusNode.requestFocus] and [FocusNode.unfocus], are not affected |
95 | /// by this enumeration. |
96 | /// |
97 | /// See also: |
98 | /// |
99 | /// * [FocusTraversalPolicy], which implements the logic behind this enum. |
100 | /// * [FocusScopeNode], which is configured by this enum. |
101 | enum TraversalEdgeBehavior { |
102 | /// Keeps the focus among the items of the focus scope. |
103 | /// |
104 | /// Requesting the next focus after the last focusable item will transfer the |
105 | /// focus to the first item, and requesting focus previous to the first item |
106 | /// will transfer the focus to the last item, thus forming a closed loop of |
107 | /// focusable items. |
108 | closedLoop, |
109 | |
110 | /// Allows the focus to leave the [FlutterView]. |
111 | /// |
112 | /// Requesting next focus after the last focusable item or previous to the |
113 | /// first item will unfocus any focused nodes. If the focus traversal action |
114 | /// was initiated by the embedder (e.g. the Flutter Engine) the embedder |
115 | /// receives a result indicating that the focus is no longer within the |
116 | /// current [FlutterView]. For example, [NextFocusAction] invoked via keyboard |
117 | /// (typically the TAB key) would receive [KeyEventResult.skipRemainingHandlers] |
118 | /// allowing the embedder handle the shortcut. On the web, typically the |
119 | /// control is transferred to the browser, allowing the user to reach the |
120 | /// address bar, escape an `iframe`, or focus on HTML elements other than |
121 | /// those managed by Flutter. |
122 | leaveFlutterView, |
123 | |
124 | /// Allows focus to traverse up to parent scope. |
125 | /// |
126 | /// When reaching the edge of the current scope, requesting the next focus |
127 | /// will look up to the parent scope of the current scope and focus the focus |
128 | /// node next to the current scope. |
129 | /// |
130 | /// If there is no parent scope above the current scope, fallback to |
131 | /// [closedLoop] behavior. |
132 | parentScope, |
133 | } |
134 | |
135 | /// Determines how focusable widgets are traversed within a [FocusTraversalGroup]. |
136 | /// |
137 | /// The focus traversal policy is what determines which widget is "next", |
138 | /// "previous", or in a direction from the widget associated with the currently |
139 | /// focused [FocusNode] (usually a [Focus] widget). |
140 | /// |
141 | /// One of the pre-defined subclasses may be used, or define a custom policy to |
142 | /// create a unique focus order. |
143 | /// |
144 | /// When defining your own, your subclass should implement [sortDescendants] to |
145 | /// provide the order in which you would like the descendants to be traversed. |
146 | /// |
147 | /// See also: |
148 | /// |
149 | /// * [FocusNode], for a description of the focus system. |
150 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
151 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
152 | /// * [FocusNode], which is affected by the traversal policy. |
153 | /// * [WidgetOrderTraversalPolicy], a policy that relies on the widget |
154 | /// creation order to describe the order of traversal. |
155 | /// * [ReadingOrderTraversalPolicy], a policy that describes the order as the |
156 | /// natural "reading order" for the current [Directionality]. |
157 | /// * [OrderedTraversalPolicy], a policy that describes the order |
158 | /// explicitly using [FocusTraversalOrder] widgets. |
159 | /// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements |
160 | /// focus traversal in a direction. |
161 | @immutable |
162 | abstract class FocusTraversalPolicy with Diagnosticable { |
163 | /// Abstract const constructor. This constructor enables subclasses to provide |
164 | /// const constructors so that they can be used in const expressions. |
165 | /// |
166 | /// {@template flutter.widgets.FocusTraversalPolicy.requestFocusCallback} |
167 | /// The `requestFocusCallback` can be used to override the default behavior |
168 | /// of the focus requests. If `requestFocusCallback` |
169 | /// is null, it defaults to [FocusTraversalPolicy.defaultTraversalRequestFocusCallback]. |
170 | /// {@endtemplate} |
171 | const FocusTraversalPolicy({ |
172 | TraversalRequestFocusCallback? requestFocusCallback |
173 | }) : requestFocusCallback = requestFocusCallback ?? defaultTraversalRequestFocusCallback; |
174 | |
175 | /// The callback used to move the focus from one focus node to another when |
176 | /// traversing them using a keyboard. By default it requests focus on the next |
177 | /// node and ensures the node is visible if it's in a scrollable. |
178 | final TraversalRequestFocusCallback requestFocusCallback; |
179 | |
180 | /// The default value for [requestFocusCallback]. |
181 | /// Requests focus from `node` and ensures the node is visible |
182 | /// by calling [Scrollable.ensureVisible]. |
183 | static void defaultTraversalRequestFocusCallback( |
184 | FocusNode node, { |
185 | ScrollPositionAlignmentPolicy? alignmentPolicy, |
186 | double? alignment, |
187 | Duration? duration, |
188 | Curve? curve, |
189 | }) { |
190 | node.requestFocus(); |
191 | Scrollable.ensureVisible( |
192 | node.context!, |
193 | alignment: alignment ?? 1, |
194 | alignmentPolicy: alignmentPolicy ?? ScrollPositionAlignmentPolicy.explicit, |
195 | duration: duration ?? Duration.zero, |
196 | curve: curve ?? Curves.ease, |
197 | ); |
198 | } |
199 | |
200 | /// Request focus on a focus node as a result of a tab traversal. |
201 | /// |
202 | /// If the `node` is a [FocusScopeNode], this method will recursively find |
203 | /// the next focus from its descendants until it find a regular [FocusNode]. |
204 | /// |
205 | /// Returns true if this method focused a new focus node. |
206 | bool _requestTabTraversalFocus( |
207 | FocusNode node, { |
208 | ScrollPositionAlignmentPolicy? alignmentPolicy, |
209 | double? alignment, |
210 | Duration? duration, |
211 | Curve? curve, |
212 | required bool forward, |
213 | }) { |
214 | if (node is FocusScopeNode) { |
215 | if (node.focusedChild != null) { |
216 | // Can't stop here as the `focusedChild` may be a focus scope node |
217 | // without a first focus. The first focus will be picked in the |
218 | // next iteration. |
219 | return _requestTabTraversalFocus( |
220 | node.focusedChild!, |
221 | alignmentPolicy: alignmentPolicy, |
222 | alignment: alignment, |
223 | duration: duration, |
224 | curve: curve, |
225 | forward: forward, |
226 | ); |
227 | } |
228 | final List<FocusNode> sortedChildren = _sortAllDescendants(node, node); |
229 | if (sortedChildren.isNotEmpty) { |
230 | _requestTabTraversalFocus( |
231 | forward ? sortedChildren.first : sortedChildren.last, |
232 | alignmentPolicy: alignmentPolicy, |
233 | alignment: alignment, |
234 | duration: duration, |
235 | curve: curve, |
236 | forward: forward, |
237 | ); |
238 | // Regardless if _requestTabTraversalFocus return true or false, a first |
239 | // focus has been picked. |
240 | return true; |
241 | } |
242 | } |
243 | final bool nodeHadPrimaryFocus = node.hasPrimaryFocus; |
244 | requestFocusCallback( |
245 | node, |
246 | alignmentPolicy: alignmentPolicy, |
247 | alignment: alignment, |
248 | duration: duration, |
249 | curve: curve, |
250 | ); |
251 | return !nodeHadPrimaryFocus; |
252 | } |
253 | |
254 | /// Returns the node that should receive focus if focus is traversing |
255 | /// forwards, and there is no current focus. |
256 | /// |
257 | /// The node returned is the node that should receive focus if focus is |
258 | /// traversing forwards (i.e. with [next]), and there is no current focus in |
259 | /// the nearest [FocusScopeNode] that `currentNode` belongs to. |
260 | /// |
261 | /// If `ignoreCurrentFocus` is false or not given, this function returns the |
262 | /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the |
263 | /// `currentNode`, otherwise, returns the first node from [sortDescendants], |
264 | /// or the given `currentNode` if there are no descendants. |
265 | /// |
266 | /// If `ignoreCurrentFocus` is true, then the algorithm returns the first node |
267 | /// from [sortDescendants], or the given `currentNode` if there are no |
268 | /// descendants. |
269 | /// |
270 | /// See also: |
271 | /// |
272 | /// * [next], the function that is called to move the focus to the next node. |
273 | /// * [DirectionalFocusTraversalPolicyMixin.findFirstFocusInDirection], a |
274 | /// function that finds the first focusable widget in a particular |
275 | /// direction. |
276 | FocusNode? findFirstFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { |
277 | return _findInitialFocus(currentNode, ignoreCurrentFocus: ignoreCurrentFocus); |
278 | } |
279 | |
280 | /// Returns the node that should receive focus if focus is traversing |
281 | /// backwards, and there is no current focus. |
282 | /// |
283 | /// The node returned is the one that should receive focus if focus is |
284 | /// traversing backwards (i.e. with [previous]), and there is no current focus |
285 | /// in the nearest [FocusScopeNode] that `currentNode` belongs to. |
286 | /// |
287 | /// If `ignoreCurrentFocus` is false or not given, this function returns the |
288 | /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the |
289 | /// `currentNode`, otherwise, returns the last node from [sortDescendants], |
290 | /// or the given `currentNode` if there are no descendants. |
291 | /// |
292 | /// If `ignoreCurrentFocus` is true, then the algorithm returns the last node |
293 | /// from [sortDescendants], or the given `currentNode` if there are no |
294 | /// descendants. |
295 | /// |
296 | /// See also: |
297 | /// |
298 | /// * [previous], the function that is called to move the focus to the previous node. |
299 | /// * [DirectionalFocusTraversalPolicyMixin.findFirstFocusInDirection], a |
300 | /// function that finds the first focusable widget in a particular direction. |
301 | FocusNode findLastFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) { |
302 | return _findInitialFocus(currentNode, fromEnd: true, ignoreCurrentFocus: ignoreCurrentFocus); |
303 | } |
304 | |
305 | FocusNode _findInitialFocus(FocusNode currentNode, {bool fromEnd = false, bool ignoreCurrentFocus = false}) { |
306 | final FocusScopeNode scope = currentNode.nearestScope!; |
307 | FocusNode? candidate = scope.focusedChild; |
308 | if (ignoreCurrentFocus || candidate == null && scope.descendants.isNotEmpty) { |
309 | final Iterable<FocusNode> sorted = _sortAllDescendants(scope, currentNode).where((FocusNode node) => _canRequestTraversalFocus(node)); |
310 | if (sorted.isEmpty) { |
311 | candidate = null; |
312 | } else { |
313 | candidate = fromEnd ? sorted.last : sorted.first; |
314 | } |
315 | } |
316 | |
317 | // If we still didn't find any candidate, use the current node as a |
318 | // fallback. |
319 | candidate ??= currentNode; |
320 | return candidate; |
321 | } |
322 | |
323 | /// Returns the first node in the given `direction` that should receive focus |
324 | /// if there is no current focus in the scope to which the `currentNode` |
325 | /// belongs. |
326 | /// |
327 | /// This is typically used by [inDirection] to determine which node to focus |
328 | /// if it is called when no node is currently focused. |
329 | FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction); |
330 | |
331 | /// Clears the data associated with the given [FocusScopeNode] for this object. |
332 | /// |
333 | /// This is used to indicate that the focus policy has changed its mode, and |
334 | /// so any cached policy data should be invalidated. For example, changing the |
335 | /// direction in which focus is moving, or changing from directional to |
336 | /// next/previous navigation modes. |
337 | /// |
338 | /// The default implementation does nothing. |
339 | @mustCallSuper |
340 | void invalidateScopeData(FocusScopeNode node) {} |
341 | |
342 | /// This is called whenever the given [node] is re-parented into a new scope, |
343 | /// so that the policy has a chance to update or invalidate any cached data |
344 | /// that it maintains per scope about the node. |
345 | /// |
346 | /// The [oldScope] is the previous scope that this node belonged to, if any. |
347 | /// |
348 | /// The default implementation does nothing. |
349 | @mustCallSuper |
350 | void changedScope({FocusNode? node, FocusScopeNode? oldScope}) {} |
351 | |
352 | /// Focuses the next widget in the focus scope that contains the given |
353 | /// [currentNode]. |
354 | /// |
355 | /// This should determine what the next node to receive focus should be by |
356 | /// inspecting the node tree, and then calling [FocusNode.requestFocus] on |
357 | /// the node that has been selected. |
358 | /// |
359 | /// Returns true if it successfully found a node and requested focus. |
360 | bool next(FocusNode currentNode) => _moveFocus(currentNode, forward: true); |
361 | |
362 | /// Focuses the previous widget in the focus scope that contains the given |
363 | /// [currentNode]. |
364 | /// |
365 | /// This should determine what the previous node to receive focus should be by |
366 | /// inspecting the node tree, and then calling [FocusNode.requestFocus] on |
367 | /// the node that has been selected. |
368 | /// |
369 | /// Returns true if it successfully found a node and requested focus. |
370 | bool previous(FocusNode currentNode) => _moveFocus(currentNode, forward: false); |
371 | |
372 | /// Focuses the next widget in the given [direction] in the focus scope that |
373 | /// contains the given [currentNode]. |
374 | /// |
375 | /// This should determine what the next node to receive focus in the given |
376 | /// [direction] should be by inspecting the node tree, and then calling |
377 | /// [FocusNode.requestFocus] on the node that has been selected. |
378 | /// |
379 | /// Returns true if it successfully found a node and requested focus. |
380 | bool inDirection(FocusNode currentNode, TraversalDirection direction); |
381 | |
382 | /// Sorts the given `descendants` into focus order. |
383 | /// |
384 | /// Subclasses should override this to implement a different sort for [next] |
385 | /// and [previous] to use in their ordering. If the returned iterable omits a |
386 | /// node that is a descendant of the given scope, then the user will be unable |
387 | /// to use next/previous keyboard traversal to reach that node. |
388 | /// |
389 | /// The node used to initiate the traversal (the one passed to [next] or |
390 | /// [previous]) is passed as `currentNode`. |
391 | /// |
392 | /// Having the current node in the list is what allows the algorithm to |
393 | /// determine which nodes are adjacent to the current node. If the |
394 | /// `currentNode` is removed from the list, then the focus will be unchanged |
395 | /// when [next] or [previous] are called, and they will return false. |
396 | /// |
397 | /// This is not used for directional focus ([inDirection]), only for |
398 | /// determining the focus order for [next] and [previous]. |
399 | /// |
400 | /// When implementing an override for this function, be sure to use |
401 | /// [mergeSort] instead of Dart's default list sorting algorithm when sorting |
402 | /// items, since the default algorithm is not stable (items deemed to be equal |
403 | /// can appear in arbitrary order, and change positions between sorts), whereas |
404 | /// [mergeSort] is stable. |
405 | @protected |
406 | Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode); |
407 | |
408 | static bool _canRequestTraversalFocus(FocusNode node) { |
409 | return node.canRequestFocus && !node.skipTraversal; |
410 | } |
411 | |
412 | static Iterable<FocusNode> _getDescendantsWithoutExpandingScope(FocusNode node) { |
413 | final List<FocusNode> result = <FocusNode>[]; |
414 | for (final FocusNode child in node.children) { |
415 | result.add(child); |
416 | if (child is! FocusScopeNode) { |
417 | result.addAll(_getDescendantsWithoutExpandingScope(child)); |
418 | } |
419 | } |
420 | return result; |
421 | } |
422 | |
423 | static Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) { |
424 | final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy(); |
425 | final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{}; |
426 | for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) { |
427 | final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node); |
428 | // Group nodes need to be added to their parent's node, or to the "null" |
429 | // node if no parent is found. This creates the hierarchy of group nodes |
430 | // and makes it so the entire group is sorted along with the other members |
431 | // of the parent group. |
432 | if (node == groupNode) { |
433 | // To find the parent of the group node, we need to skip over the parent |
434 | // of the Focus node added in _FocusTraversalGroupState.build, and start |
435 | // looking with that node's parent, since _getGroupNode will return the |
436 | // node it was called on if it matches the type. |
437 | final _FocusTraversalGroupNode? parentGroup = FocusTraversalGroup._getGroupNode(groupNode!.parent!); |
438 | groups[parentGroup] ??= _FocusTraversalGroupInfo(parentGroup, members: <FocusNode>[], defaultPolicy: defaultPolicy); |
439 | assert(!groups[parentGroup]!.members.contains(node)); |
440 | groups[parentGroup]!.members.add(groupNode); |
441 | continue; |
442 | } |
443 | // Skip non-focusable and non-traversable nodes in the same way that |
444 | // FocusScopeNode.traversalDescendants would. |
445 | // |
446 | // Current focused node needs to be in the group so that the caller can |
447 | // find the next traversable node from the current focused node. |
448 | if (node == currentNode || (node.canRequestFocus && !node.skipTraversal)) { |
449 | groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: <FocusNode>[], defaultPolicy: defaultPolicy); |
450 | assert(!groups[groupNode]!.members.contains(node)); |
451 | groups[groupNode]!.members.add(node); |
452 | } |
453 | } |
454 | return groups; |
455 | } |
456 | |
457 | // Sort all descendants, taking into account the FocusTraversalGroup |
458 | // that they are each in, and filtering out non-traversable/focusable nodes. |
459 | static List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) { |
460 | final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope); |
461 | // Build the sorting data structure, separating descendants into groups. |
462 | final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode); |
463 | |
464 | // Sort the member lists using the individual policy sorts. |
465 | for (final FocusNode? key in groups.keys) { |
466 | final List<FocusNode> sortedMembers = groups[key]!.policy.sortDescendants(groups[key]!.members, currentNode).toList(); |
467 | groups[key]!.members.clear(); |
468 | groups[key]!.members.addAll(sortedMembers); |
469 | } |
470 | |
471 | // Traverse the group tree, adding the children of members in the order they |
472 | // appear in the member lists. |
473 | final List<FocusNode> sortedDescendants = <FocusNode>[]; |
474 | void visitGroups(_FocusTraversalGroupInfo info) { |
475 | for (final FocusNode node in info.members) { |
476 | if (groups.containsKey(node)) { |
477 | // This is a policy group focus node. Replace it with the members of |
478 | // the corresponding policy group. |
479 | visitGroups(groups[node]!); |
480 | } else { |
481 | sortedDescendants.add(node); |
482 | } |
483 | } |
484 | } |
485 | |
486 | // Visit the children of the scope, if any. |
487 | if (groups.isNotEmpty && groups.containsKey(scopeGroupNode)) { |
488 | visitGroups(groups[scopeGroupNode]!); |
489 | } |
490 | |
491 | // Remove the FocusTraversalGroup nodes themselves, which aren't focusable. |
492 | // They were left in above because they were needed to find their members |
493 | // during sorting. |
494 | sortedDescendants.removeWhere((FocusNode node) { |
495 | return node != currentNode && !_canRequestTraversalFocus(node); |
496 | }); |
497 | |
498 | // Sanity check to make sure that the algorithm above doesn't diverge from |
499 | // the one in FocusScopeNode.traversalDescendants in terms of which nodes it |
500 | // finds. |
501 | assert((){ |
502 | final Set<FocusNode> difference = sortedDescendants.toSet().difference(scope.traversalDescendants.toSet()); |
503 | if (!_canRequestTraversalFocus(currentNode)) { |
504 | // The scope.traversalDescendants will not contain currentNode if it |
505 | // skips traversal or not focusable. |
506 | assert( |
507 | difference.isEmpty || (difference.length == 1 && difference.contains(currentNode)), |
508 | 'Difference between sorted descendants and FocusScopeNode.traversalDescendants contains ' |
509 | 'something other than the current skipped node. This is the difference: $difference' , |
510 | ); |
511 | return true; |
512 | } |
513 | assert( |
514 | difference.isEmpty, |
515 | 'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. ' |
516 | 'These are the different nodes: $difference' , |
517 | ); |
518 | return true; |
519 | }()); |
520 | return sortedDescendants; |
521 | } |
522 | |
523 | /// Moves the focus to the next node in the FocusScopeNode nearest to the |
524 | /// currentNode argument, either in a forward or reverse direction, depending |
525 | /// on the value of the forward argument. |
526 | /// |
527 | /// This function is called by the next and previous members to move to the |
528 | /// next or previous node, respectively. |
529 | /// |
530 | /// Uses [findFirstFocus]/[findLastFocus] to find the first/last node if there is |
531 | /// no [FocusScopeNode.focusedChild] set. If there is a focused child for the |
532 | /// scope, then it calls sortDescendants to get a sorted list of descendants, |
533 | /// and then finds the node after the current first focus of the scope if |
534 | /// forward is true, and the node before it if forward is false. |
535 | /// |
536 | /// Returns true if a node requested focus. |
537 | @protected |
538 | bool _moveFocus(FocusNode currentNode, {required bool forward}) { |
539 | final FocusScopeNode nearestScope = currentNode.nearestScope!; |
540 | invalidateScopeData(nearestScope); |
541 | FocusNode? focusedChild = nearestScope.focusedChild; |
542 | if (focusedChild == null) { |
543 | final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode); |
544 | if (firstFocus != null) { |
545 | return _requestTabTraversalFocus( |
546 | firstFocus, |
547 | alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
548 | forward: forward, |
549 | ); |
550 | } |
551 | } |
552 | focusedChild ??= nearestScope; |
553 | final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild); |
554 | assert(sortedNodes.contains(focusedChild)); |
555 | |
556 | if (forward && focusedChild == sortedNodes.last) { |
557 | switch (nearestScope.traversalEdgeBehavior) { |
558 | case TraversalEdgeBehavior.leaveFlutterView: |
559 | focusedChild.unfocus(); |
560 | return false; |
561 | case TraversalEdgeBehavior.parentScope: |
562 | final FocusScopeNode? parentScope = nearestScope.enclosingScope; |
563 | if (parentScope != null && parentScope != FocusManager.instance.rootScope) { |
564 | focusedChild.unfocus(); |
565 | parentScope.nextFocus(); |
566 | // Verify the focus really has changed. |
567 | return focusedChild.enclosingScope?.focusedChild != focusedChild; |
568 | } |
569 | // No valid parent scope. Fallback to closed loop behavior. |
570 | return _requestTabTraversalFocus( |
571 | sortedNodes.first, |
572 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
573 | forward: forward, |
574 | ); |
575 | case TraversalEdgeBehavior.closedLoop: |
576 | return _requestTabTraversalFocus( |
577 | sortedNodes.first, |
578 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
579 | forward: forward, |
580 | ); |
581 | } |
582 | } |
583 | if (!forward && focusedChild == sortedNodes.first) { |
584 | switch (nearestScope.traversalEdgeBehavior) { |
585 | case TraversalEdgeBehavior.leaveFlutterView: |
586 | focusedChild.unfocus(); |
587 | return false; |
588 | case TraversalEdgeBehavior.parentScope: |
589 | final FocusScopeNode? parentScope = nearestScope.enclosingScope; |
590 | if (parentScope != null && parentScope != FocusManager.instance.rootScope) { |
591 | focusedChild.unfocus(); |
592 | parentScope.previousFocus(); |
593 | // Verify the focus really has changed. |
594 | return focusedChild.enclosingScope?.focusedChild != focusedChild; |
595 | } |
596 | // No valid parent scope. Fallback to closed loop behavior. |
597 | return _requestTabTraversalFocus( |
598 | sortedNodes.last, |
599 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
600 | forward: forward, |
601 | ); |
602 | case TraversalEdgeBehavior.closedLoop: |
603 | return _requestTabTraversalFocus( |
604 | sortedNodes.last, |
605 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
606 | forward: forward, |
607 | ); |
608 | } |
609 | } |
610 | |
611 | final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed; |
612 | FocusNode? previousNode; |
613 | for (final FocusNode node in maybeFlipped) { |
614 | if (previousNode == focusedChild) { |
615 | return _requestTabTraversalFocus( |
616 | node, |
617 | alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
618 | forward: forward, |
619 | ); |
620 | } |
621 | previousNode = node; |
622 | } |
623 | return false; |
624 | } |
625 | } |
626 | |
627 | // A policy data object for use by the DirectionalFocusTraversalPolicyMixin so |
628 | // it can keep track of the traversal history. |
629 | class _DirectionalPolicyDataEntry { |
630 | const _DirectionalPolicyDataEntry({required this.direction, required this.node}); |
631 | |
632 | final TraversalDirection direction; |
633 | final FocusNode node; |
634 | } |
635 | |
636 | class _DirectionalPolicyData { |
637 | const _DirectionalPolicyData({required this.history}); |
638 | |
639 | /// A queue of entries that describe the path taken to the current node. |
640 | final List<_DirectionalPolicyDataEntry> history; |
641 | } |
642 | |
643 | /// A mixin class that provides an implementation for finding a node in a |
644 | /// particular direction. |
645 | /// |
646 | /// This can be mixed in to other [FocusTraversalPolicy] implementations that |
647 | /// only want to implement new next/previous policies. |
648 | /// |
649 | /// Since hysteresis in the navigation order is undesirable, this implementation |
650 | /// maintains a stack of previous locations that have been visited on the policy |
651 | /// data for the affected [FocusScopeNode]. If the previous direction was the |
652 | /// opposite of the current direction, then the this policy will request focus |
653 | /// on the previously focused node. Change to another direction other than the |
654 | /// current one or its opposite will clear the stack. |
655 | /// |
656 | /// For instance, if the focus moves down, down, down, and then up, up, up, it |
657 | /// will follow the same path through the widgets in both directions. However, |
658 | /// if it moves down, down, down, left, right, and then up, up, up, it may not |
659 | /// follow the same path on the way up as it did on the way down, since changing |
660 | /// the axis of motion resets the history. |
661 | /// |
662 | /// This class implements an algorithm that considers an infinite band extending |
663 | /// along the direction of movement, the width or height (depending on |
664 | /// direction) of the currently focused widget, and finds the closest widget in |
665 | /// that band along the direction of movement. If nothing is found in that band, |
666 | /// then it picks the widget with an edge closest to the band in the |
667 | /// perpendicular direction. If two out-of-band widgets are the same distance |
668 | /// from the band, then it picks the one closest along the direction of |
669 | /// movement. |
670 | /// |
671 | /// The goal of this algorithm is to pick a widget that (to the user) doesn't |
672 | /// appear to traverse along the wrong axis, as it might if it only sorted |
673 | /// widgets by distance along one axis, but also jumps to the next logical |
674 | /// widget in a direction without skipping over widgets. |
675 | /// |
676 | /// See also: |
677 | /// |
678 | /// * [FocusNode], for a description of the focus system. |
679 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
680 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
681 | /// * [WidgetOrderTraversalPolicy], a policy that relies on the widget creation |
682 | /// order to describe the order of traversal. |
683 | /// * [ReadingOrderTraversalPolicy], a policy that describes the order as the |
684 | /// natural "reading order" for the current [Directionality]. |
685 | /// * [OrderedTraversalPolicy], a policy that describes the order explicitly |
686 | /// using [FocusTraversalOrder] widgets. |
687 | mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { |
688 | final Map<FocusScopeNode, _DirectionalPolicyData> _policyData = <FocusScopeNode, _DirectionalPolicyData>{}; |
689 | |
690 | @override |
691 | void invalidateScopeData(FocusScopeNode node) { |
692 | super.invalidateScopeData(node); |
693 | _policyData.remove(node); |
694 | } |
695 | |
696 | @override |
697 | void changedScope({FocusNode? node, FocusScopeNode? oldScope}) { |
698 | super.changedScope(node: node, oldScope: oldScope); |
699 | if (oldScope != null) { |
700 | _policyData[oldScope]?.history.removeWhere((_DirectionalPolicyDataEntry entry) { |
701 | return entry.node == node; |
702 | }); |
703 | } |
704 | } |
705 | |
706 | @override |
707 | FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) { |
708 | switch (direction) { |
709 | case TraversalDirection.up: |
710 | // Find the bottom-most node so we can go up from there. |
711 | return _sortAndFindInitial(currentNode, vertical: true, first: false); |
712 | case TraversalDirection.down: |
713 | // Find the top-most node so we can go down from there. |
714 | return _sortAndFindInitial(currentNode, vertical: true, first: true); |
715 | case TraversalDirection.left: |
716 | // Find the right-most node so we can go left from there. |
717 | return _sortAndFindInitial(currentNode, vertical: false, first: false); |
718 | case TraversalDirection.right: |
719 | // Find the left-most node so we can go right from there. |
720 | return _sortAndFindInitial(currentNode, vertical: false, first: true); |
721 | } |
722 | } |
723 | |
724 | FocusNode? _sortAndFindInitial(FocusNode currentNode, {required bool vertical, required bool first}) { |
725 | final Iterable<FocusNode> nodes = currentNode.nearestScope!.traversalDescendants; |
726 | final List<FocusNode> sorted = nodes.toList(); |
727 | mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) { |
728 | if (vertical) { |
729 | if (first) { |
730 | return a.rect.top.compareTo(b.rect.top); |
731 | } else { |
732 | return b.rect.bottom.compareTo(a.rect.bottom); |
733 | } |
734 | } else { |
735 | if (first) { |
736 | return a.rect.left.compareTo(b.rect.left); |
737 | } else { |
738 | return b.rect.right.compareTo(a.rect.right); |
739 | } |
740 | } |
741 | }); |
742 | |
743 | if (sorted.isNotEmpty) { |
744 | return sorted.first; |
745 | } |
746 | |
747 | return null; |
748 | } |
749 | |
750 | static int _verticalCompare(Offset target, Offset a, Offset b) { |
751 | return (a.dy - target.dy).abs().compareTo((b.dy - target.dy).abs()); |
752 | } |
753 | |
754 | static int _horizontalCompare(Offset target, Offset a, Offset b) { |
755 | return (a.dx - target.dx).abs().compareTo((b.dx - target.dx).abs()); |
756 | } |
757 | |
758 | // Sort the ones that are closest to target vertically first, and if two are |
759 | // the same vertical distance, pick the one that is closest horizontally. |
760 | static Iterable<FocusNode> _sortByDistancePreferVertical(Offset target, Iterable<FocusNode> nodes) { |
761 | final List<FocusNode> sorted = nodes.toList(); |
762 | mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) { |
763 | final Offset a = nodeA.rect.center; |
764 | final Offset b = nodeB.rect.center; |
765 | final int vertical = _verticalCompare(target, a, b); |
766 | if (vertical == 0) { |
767 | return _horizontalCompare(target, a, b); |
768 | } |
769 | return vertical; |
770 | }); |
771 | return sorted; |
772 | } |
773 | |
774 | // Sort the ones that are closest horizontally first, and if two are the same |
775 | // horizontal distance, pick the one that is closest vertically. |
776 | static Iterable<FocusNode> _sortByDistancePreferHorizontal(Offset target, Iterable<FocusNode> nodes) { |
777 | final List<FocusNode> sorted = nodes.toList(); |
778 | mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) { |
779 | final Offset a = nodeA.rect.center; |
780 | final Offset b = nodeB.rect.center; |
781 | final int horizontal = _horizontalCompare(target, a, b); |
782 | if (horizontal == 0) { |
783 | return _verticalCompare(target, a, b); |
784 | } |
785 | return horizontal; |
786 | }); |
787 | return sorted; |
788 | } |
789 | |
790 | static int _verticalCompareClosestEdge(Offset target, Rect a, Rect b) { |
791 | // Find which edge is closest to the target for each. |
792 | final double aCoord = (a.top - target.dy).abs() < (a.bottom - target.dy).abs() ? a.top : a.bottom; |
793 | final double bCoord = (b.top - target.dy).abs() < (b.bottom - target.dy).abs() ? b.top : b.bottom; |
794 | return (aCoord - target.dy).abs().compareTo((bCoord - target.dy).abs()); |
795 | } |
796 | |
797 | static int _horizontalCompareClosestEdge(Offset target, Rect a, Rect b) { |
798 | // Find which edge is closest to the target for each. |
799 | final double aCoord = (a.left - target.dx).abs() < (a.right - target.dx).abs() ? a.left : a.right; |
800 | final double bCoord = (b.left - target.dx).abs() < (b.right - target.dx).abs() ? b.left : b.right; |
801 | return (aCoord - target.dx).abs().compareTo((bCoord - target.dx).abs()); |
802 | } |
803 | |
804 | // Sort the ones that have edges that are closest horizontally first, and if |
805 | // two are the same horizontal distance, pick the one that is closest |
806 | // vertically. |
807 | static Iterable<FocusNode> _sortClosestEdgesByDistancePreferHorizontal(Offset target, Iterable<FocusNode> nodes) { |
808 | final List<FocusNode> sorted = nodes.toList(); |
809 | mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) { |
810 | final int horizontal = _horizontalCompareClosestEdge(target, nodeA.rect, nodeB.rect); |
811 | if (horizontal == 0) { |
812 | // If they're the same distance horizontally, pick the closest one |
813 | // vertically. |
814 | return _verticalCompare(target, nodeA.rect.center, nodeB.rect.center); |
815 | } |
816 | return horizontal; |
817 | }); |
818 | return sorted; |
819 | } |
820 | |
821 | // Sort the ones that have edges that are closest vertically first, and if |
822 | // two are the same vertical distance, pick the one that is closest |
823 | // horizontally. |
824 | static Iterable<FocusNode> _sortClosestEdgesByDistancePreferVertical(Offset target, Iterable<FocusNode> nodes) { |
825 | final List<FocusNode> sorted = nodes.toList(); |
826 | mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) { |
827 | final int vertical = _verticalCompareClosestEdge(target, nodeA.rect, nodeB.rect); |
828 | if (vertical == 0) { |
829 | // If they're the same distance vertically, pick the closest one |
830 | // horizontally. |
831 | return _horizontalCompare(target, nodeA.rect.center, nodeB.rect.center); |
832 | } |
833 | return vertical; |
834 | }); |
835 | return sorted; |
836 | } |
837 | |
838 | // Sorts nodes from left to right horizontally, and removes nodes that are |
839 | // either to the right of the left side of the target node if we're going |
840 | // left, or to the left of the right side of the target node if we're going |
841 | // right. |
842 | // |
843 | // This doesn't need to take into account directionality because it is |
844 | // typically intending to actually go left or right, not in a reading |
845 | // direction. |
846 | Iterable<FocusNode> _sortAndFilterHorizontally( |
847 | TraversalDirection direction, |
848 | Rect target, |
849 | Iterable<FocusNode> nodes, |
850 | ) { |
851 | assert(direction == TraversalDirection.left || direction == TraversalDirection.right); |
852 | final Iterable<FocusNode> filtered; |
853 | switch (direction) { |
854 | case TraversalDirection.left: |
855 | filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left); |
856 | case TraversalDirection.right: |
857 | filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right); |
858 | case TraversalDirection.up: |
859 | case TraversalDirection.down: |
860 | throw ArgumentError('Invalid direction $direction' ); |
861 | } |
862 | final List<FocusNode> sorted = filtered.toList(); |
863 | // Sort all nodes from left to right. |
864 | mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx)); |
865 | return sorted; |
866 | } |
867 | |
868 | // Sorts nodes from top to bottom vertically, and removes nodes that are |
869 | // either below the top of the target node if we're going up, or above the |
870 | // bottom of the target node if we're going down. |
871 | Iterable<FocusNode> _sortAndFilterVertically( |
872 | TraversalDirection direction, |
873 | Rect target, |
874 | Iterable<FocusNode> nodes, |
875 | ) { |
876 | assert(direction == TraversalDirection.up || direction == TraversalDirection.down); |
877 | final Iterable<FocusNode> filtered; |
878 | switch (direction) { |
879 | case TraversalDirection.up: |
880 | filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top); |
881 | case TraversalDirection.down: |
882 | filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom); |
883 | case TraversalDirection.left: |
884 | case TraversalDirection.right: |
885 | throw ArgumentError('Invalid direction $direction' ); |
886 | } |
887 | final List<FocusNode> sorted = filtered.toList(); |
888 | mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy)); |
889 | return sorted; |
890 | } |
891 | |
892 | // Updates the policy data to keep the previously visited node so that we can |
893 | // avoid hysteresis when we change directions in navigation. |
894 | // |
895 | // Returns true if focus was requested on a previous node. |
896 | bool _popPolicyDataIfNeeded(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) { |
897 | final _DirectionalPolicyData? policyData = _policyData[nearestScope]; |
898 | if (policyData != null && policyData.history.isNotEmpty && policyData.history.first.direction != direction) { |
899 | if (policyData.history.last.node.parent == null) { |
900 | // If a node has been removed from the tree, then we should stop |
901 | // referencing it and reset the scope data so that we don't try and |
902 | // request focus on it. This can happen in slivers where the rendered |
903 | // node has been unmounted. This has the side effect that hysteresis |
904 | // might not be avoided when items that go off screen get unmounted. |
905 | invalidateScopeData(nearestScope); |
906 | return false; |
907 | } |
908 | |
909 | // Returns true if successfully popped the history. |
910 | bool popOrInvalidate(TraversalDirection direction) { |
911 | final FocusNode lastNode = policyData.history.removeLast().node; |
912 | if (Scrollable.maybeOf(lastNode.context!) != Scrollable.maybeOf(primaryFocus!.context!)) { |
913 | invalidateScopeData(nearestScope); |
914 | return false; |
915 | } |
916 | final ScrollPositionAlignmentPolicy alignmentPolicy; |
917 | switch (direction) { |
918 | case TraversalDirection.up: |
919 | case TraversalDirection.left: |
920 | alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtStart; |
921 | case TraversalDirection.right: |
922 | case TraversalDirection.down: |
923 | alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd; |
924 | } |
925 | requestFocusCallback( |
926 | lastNode, |
927 | alignmentPolicy: alignmentPolicy, |
928 | ); |
929 | return true; |
930 | } |
931 | |
932 | switch (direction) { |
933 | case TraversalDirection.down: |
934 | case TraversalDirection.up: |
935 | switch (policyData.history.first.direction) { |
936 | case TraversalDirection.left: |
937 | case TraversalDirection.right: |
938 | // Reset the policy data if we change directions. |
939 | invalidateScopeData(nearestScope); |
940 | case TraversalDirection.up: |
941 | case TraversalDirection.down: |
942 | if (popOrInvalidate(direction)) { |
943 | return true; |
944 | } |
945 | } |
946 | case TraversalDirection.left: |
947 | case TraversalDirection.right: |
948 | switch (policyData.history.first.direction) { |
949 | case TraversalDirection.left: |
950 | case TraversalDirection.right: |
951 | if (popOrInvalidate(direction)) { |
952 | return true; |
953 | } |
954 | case TraversalDirection.up: |
955 | case TraversalDirection.down: |
956 | // Reset the policy data if we change directions. |
957 | invalidateScopeData(nearestScope); |
958 | } |
959 | } |
960 | } |
961 | if (policyData != null && policyData.history.isEmpty) { |
962 | invalidateScopeData(nearestScope); |
963 | } |
964 | return false; |
965 | } |
966 | |
967 | void _pushPolicyData(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) { |
968 | final _DirectionalPolicyData? policyData = _policyData[nearestScope]; |
969 | final _DirectionalPolicyDataEntry newEntry = _DirectionalPolicyDataEntry(node: focusedChild, direction: direction); |
970 | if (policyData != null) { |
971 | policyData.history.add(newEntry); |
972 | } else { |
973 | _policyData[nearestScope] = _DirectionalPolicyData(history: <_DirectionalPolicyDataEntry>[newEntry]); |
974 | } |
975 | } |
976 | |
977 | /// Focuses the next widget in the given [direction] in the [FocusScope] that |
978 | /// contains the [currentNode]. |
979 | /// |
980 | /// This determines what the next node to receive focus in the given |
981 | /// [direction] will be by inspecting the node tree, and then calling |
982 | /// [FocusNode.requestFocus] on it. |
983 | /// |
984 | /// Returns true if it successfully found a node and requested focus. |
985 | /// |
986 | /// Maintains a stack of previous locations that have been visited on the |
987 | /// policy data for the affected [FocusScopeNode]. If the previous direction |
988 | /// was the opposite of the current direction, then the this policy will |
989 | /// request focus on the previously focused node. Change to another direction |
990 | /// other than the current one or its opposite will clear the stack. |
991 | /// |
992 | /// If this function returns true when called by a subclass, then the subclass |
993 | /// should return true and not request focus from any node. |
994 | @mustCallSuper |
995 | @override |
996 | bool inDirection(FocusNode currentNode, TraversalDirection direction) { |
997 | final FocusScopeNode nearestScope = currentNode.nearestScope!; |
998 | final FocusNode? focusedChild = nearestScope.focusedChild; |
999 | if (focusedChild == null) { |
1000 | final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction) ?? currentNode; |
1001 | switch (direction) { |
1002 | case TraversalDirection.up: |
1003 | case TraversalDirection.left: |
1004 | requestFocusCallback( |
1005 | firstFocus, |
1006 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
1007 | ); |
1008 | case TraversalDirection.right: |
1009 | case TraversalDirection.down: |
1010 | requestFocusCallback( |
1011 | firstFocus, |
1012 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
1013 | ); |
1014 | } |
1015 | return true; |
1016 | } |
1017 | if (_popPolicyDataIfNeeded(direction, nearestScope, focusedChild)) { |
1018 | return true; |
1019 | } |
1020 | FocusNode? found; |
1021 | final ScrollableState? focusedScrollable = Scrollable.maybeOf(focusedChild.context!); |
1022 | switch (direction) { |
1023 | case TraversalDirection.down: |
1024 | case TraversalDirection.up: |
1025 | Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically(direction, focusedChild.rect, nearestScope.traversalDescendants); |
1026 | if (eligibleNodes.isEmpty) { |
1027 | break; |
1028 | } |
1029 | if (focusedScrollable != null && !focusedScrollable.position.atEdge) { |
1030 | final Iterable<FocusNode> filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.maybeOf(node.context!) == focusedScrollable); |
1031 | if (filteredEligibleNodes.isNotEmpty) { |
1032 | eligibleNodes = filteredEligibleNodes; |
1033 | } |
1034 | } |
1035 | if (direction == TraversalDirection.up) { |
1036 | eligibleNodes = eligibleNodes.toList().reversed; |
1037 | } |
1038 | // Find any nodes that intersect the band of the focused child. |
1039 | final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity); |
1040 | final Iterable<FocusNode> inBand = eligibleNodes.where((FocusNode node) => !node.rect.intersect(band).isEmpty); |
1041 | if (inBand.isNotEmpty) { |
1042 | found = _sortByDistancePreferVertical(focusedChild.rect.center, inBand).first; |
1043 | break; |
1044 | } |
1045 | // Only out-of-band targets are eligible, so pick the one that is |
1046 | // closest to the center line horizontally, and if any are the same |
1047 | // distance horizontally, pick the closest one of those vertically. |
1048 | found = _sortClosestEdgesByDistancePreferHorizontal(focusedChild.rect.center, eligibleNodes).first; |
1049 | case TraversalDirection.right: |
1050 | case TraversalDirection.left: |
1051 | Iterable<FocusNode> eligibleNodes = _sortAndFilterHorizontally(direction, focusedChild.rect, nearestScope.traversalDescendants); |
1052 | if (eligibleNodes.isEmpty) { |
1053 | break; |
1054 | } |
1055 | if (focusedScrollable != null && !focusedScrollable.position.atEdge) { |
1056 | final Iterable<FocusNode> filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.maybeOf(node.context!) == focusedScrollable); |
1057 | if (filteredEligibleNodes.isNotEmpty) { |
1058 | eligibleNodes = filteredEligibleNodes; |
1059 | } |
1060 | } |
1061 | if (direction == TraversalDirection.left) { |
1062 | eligibleNodes = eligibleNodes.toList().reversed; |
1063 | } |
1064 | // Find any nodes that intersect the band of the focused child. |
1065 | final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom); |
1066 | final Iterable<FocusNode> inBand = eligibleNodes.where((FocusNode node) => !node.rect.intersect(band).isEmpty); |
1067 | if (inBand.isNotEmpty) { |
1068 | found = _sortByDistancePreferHorizontal(focusedChild.rect.center, inBand).first; |
1069 | break; |
1070 | } |
1071 | // Only out-of-band targets are eligible, so pick the one that is |
1072 | // closest to the center line vertically, and if any are the same |
1073 | // distance vertically, pick the closest one of those horizontally. |
1074 | found = _sortClosestEdgesByDistancePreferVertical(focusedChild.rect.center, eligibleNodes).first; |
1075 | } |
1076 | if (found != null) { |
1077 | _pushPolicyData(direction, nearestScope, focusedChild); |
1078 | switch (direction) { |
1079 | case TraversalDirection.up: |
1080 | case TraversalDirection.left: |
1081 | requestFocusCallback( |
1082 | found, |
1083 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart, |
1084 | ); |
1085 | case TraversalDirection.down: |
1086 | case TraversalDirection.right: |
1087 | requestFocusCallback( |
1088 | found, |
1089 | alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd, |
1090 | ); |
1091 | } |
1092 | return true; |
1093 | } |
1094 | return false; |
1095 | } |
1096 | } |
1097 | |
1098 | /// A [FocusTraversalPolicy] that traverses the focus order in widget hierarchy |
1099 | /// order. |
1100 | /// |
1101 | /// This policy is used when the order desired is the order in which widgets are |
1102 | /// created in the widget hierarchy. |
1103 | /// |
1104 | /// See also: |
1105 | /// |
1106 | /// * [FocusNode], for a description of the focus system. |
1107 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
1108 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
1109 | /// * [ReadingOrderTraversalPolicy], a policy that describes the order as the |
1110 | /// natural "reading order" for the current [Directionality]. |
1111 | /// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements |
1112 | /// focus traversal in a direction. |
1113 | /// * [OrderedTraversalPolicy], a policy that describes the order |
1114 | /// explicitly using [FocusTraversalOrder] widgets. |
1115 | class WidgetOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { |
1116 | /// Constructs a traversal policy that orders widgets for keyboard traversal |
1117 | /// based on the widget hierarchy order. |
1118 | /// |
1119 | /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback} |
1120 | WidgetOrderTraversalPolicy({super.requestFocusCallback}); |
1121 | @override |
1122 | Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) => descendants; |
1123 | } |
1124 | |
1125 | // This class exists mainly for efficiency reasons: the rect is copied out of |
1126 | // the node, because it will be accessed many times in the reading order |
1127 | // algorithm, and the FocusNode.rect accessor does coordinate transformation. If |
1128 | // not for this optimization, it could just be removed, and the node used |
1129 | // directly. |
1130 | // |
1131 | // It's also a convenient place to put some utility functions having to do with |
1132 | // the sort data. |
1133 | class _ReadingOrderSortData with Diagnosticable { |
1134 | _ReadingOrderSortData(this.node) |
1135 | : rect = node.rect, |
1136 | directionality = _findDirectionality(node.context!); |
1137 | |
1138 | final TextDirection? directionality; |
1139 | final Rect rect; |
1140 | final FocusNode node; |
1141 | |
1142 | // Find the directionality in force for a build context without creating a |
1143 | // dependency. |
1144 | static TextDirection? _findDirectionality(BuildContext context) { |
1145 | return context.getInheritedWidgetOfExactType<Directionality>()?.textDirection; |
1146 | } |
1147 | |
1148 | /// Finds the common Directional ancestor of an entire list of groups. |
1149 | static TextDirection? commonDirectionalityOf(List<_ReadingOrderSortData> list) { |
1150 | final Iterable<Set<Directionality>> allAncestors = list.map<Set<Directionality>>((_ReadingOrderSortData member) => member.directionalAncestors.toSet()); |
1151 | Set<Directionality>? common; |
1152 | for (final Set<Directionality> ancestorSet in allAncestors) { |
1153 | common ??= ancestorSet; |
1154 | common = common.intersection(ancestorSet); |
1155 | } |
1156 | if (common!.isEmpty) { |
1157 | // If there is no common ancestor, then arbitrarily pick the |
1158 | // directionality of the first group, which is the equivalent of the |
1159 | // "first strongly typed" item in a bidirectional algorithm. |
1160 | return list.first.directionality; |
1161 | } |
1162 | // Find the closest common ancestor. The memberAncestors list contains the |
1163 | // ancestors for all members, but the first member's ancestry was |
1164 | // added in order from nearest to furthest, so we can still use that |
1165 | // to determine the closest one. |
1166 | return list.first.directionalAncestors.firstWhere(common.contains).textDirection; |
1167 | } |
1168 | |
1169 | static void sortWithDirectionality(List<_ReadingOrderSortData> list, TextDirection directionality) { |
1170 | mergeSort<_ReadingOrderSortData>(list, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) { |
1171 | switch (directionality) { |
1172 | case TextDirection.ltr: |
1173 | return a.rect.left.compareTo(b.rect.left); |
1174 | case TextDirection.rtl: |
1175 | return b.rect.right.compareTo(a.rect.right); |
1176 | } |
1177 | }); |
1178 | } |
1179 | |
1180 | /// Returns the list of Directionality ancestors, in order from nearest to |
1181 | /// furthest. |
1182 | Iterable<Directionality> get directionalAncestors { |
1183 | List<Directionality> getDirectionalityAncestors(BuildContext context) { |
1184 | final List<Directionality> result = <Directionality>[]; |
1185 | InheritedElement? directionalityElement = context.getElementForInheritedWidgetOfExactType<Directionality>(); |
1186 | while (directionalityElement != null) { |
1187 | result.add(directionalityElement.widget as Directionality); |
1188 | directionalityElement = _getAncestor(directionalityElement)?.getElementForInheritedWidgetOfExactType<Directionality>(); |
1189 | } |
1190 | return result; |
1191 | } |
1192 | |
1193 | _directionalAncestors ??= getDirectionalityAncestors(node.context!); |
1194 | return _directionalAncestors!; |
1195 | } |
1196 | |
1197 | List<Directionality>? _directionalAncestors; |
1198 | |
1199 | @override |
1200 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1201 | super.debugFillProperties(properties); |
1202 | properties.add(DiagnosticsProperty<TextDirection>('directionality' , directionality)); |
1203 | properties.add(StringProperty('name' , node.debugLabel, defaultValue: null)); |
1204 | properties.add(DiagnosticsProperty<Rect>('rect' , rect)); |
1205 | } |
1206 | } |
1207 | |
1208 | // A class for containing group data while sorting in reading order while taking |
1209 | // into account the ambient directionality. |
1210 | class _ReadingOrderDirectionalGroupData with Diagnosticable { |
1211 | _ReadingOrderDirectionalGroupData(this.members); |
1212 | |
1213 | final List<_ReadingOrderSortData> members; |
1214 | |
1215 | TextDirection? get directionality => members.first.directionality; |
1216 | |
1217 | Rect? _rect; |
1218 | Rect get rect { |
1219 | if (_rect == null) { |
1220 | for (final Rect rect in members.map<Rect>((_ReadingOrderSortData data) => data.rect)) { |
1221 | _rect ??= rect; |
1222 | _rect = _rect!.expandToInclude(rect); |
1223 | } |
1224 | } |
1225 | return _rect!; |
1226 | } |
1227 | |
1228 | List<Directionality> get memberAncestors { |
1229 | if (_memberAncestors == null) { |
1230 | _memberAncestors = <Directionality>[]; |
1231 | for (final _ReadingOrderSortData member in members) { |
1232 | _memberAncestors!.addAll(member.directionalAncestors); |
1233 | } |
1234 | } |
1235 | return _memberAncestors!; |
1236 | } |
1237 | |
1238 | List<Directionality>? _memberAncestors; |
1239 | |
1240 | static void sortWithDirectionality(List<_ReadingOrderDirectionalGroupData> list, TextDirection directionality) { |
1241 | mergeSort<_ReadingOrderDirectionalGroupData>(list, compare: (_ReadingOrderDirectionalGroupData a, _ReadingOrderDirectionalGroupData b) { |
1242 | switch (directionality) { |
1243 | case TextDirection.ltr: |
1244 | return a.rect.left.compareTo(b.rect.left); |
1245 | case TextDirection.rtl: |
1246 | return b.rect.right.compareTo(a.rect.right); |
1247 | } |
1248 | }); |
1249 | } |
1250 | |
1251 | @override |
1252 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1253 | super.debugFillProperties(properties); |
1254 | properties.add(DiagnosticsProperty<TextDirection>('directionality' , directionality)); |
1255 | properties.add(DiagnosticsProperty<Rect>('rect' , rect)); |
1256 | properties.add(IterableProperty<String>('members' , members.map<String>((_ReadingOrderSortData member) { |
1257 | return '" ${member.node.debugLabel}"( ${member.rect})' ; |
1258 | }))); |
1259 | } |
1260 | } |
1261 | |
1262 | /// Traverses the focus order in "reading order". |
1263 | /// |
1264 | /// By default, reading order traversal goes in the reading direction, and then |
1265 | /// down, using this algorithm: |
1266 | /// |
1267 | /// 1. Find the node rectangle that has the highest `top` on the screen. |
1268 | /// 2. Find any other nodes that intersect the infinite horizontal band defined |
1269 | /// by the highest rectangle's top and bottom edges. |
1270 | /// 3. Pick the closest to the beginning of the reading order from among the |
1271 | /// nodes discovered above. |
1272 | /// |
1273 | /// It uses the ambient [Directionality] in the context for the enclosing |
1274 | /// [FocusTraversalGroup] to determine which direction is "reading order". |
1275 | /// |
1276 | /// See also: |
1277 | /// |
1278 | /// * [FocusNode], for a description of the focus system. |
1279 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
1280 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
1281 | /// * [WidgetOrderTraversalPolicy], a policy that relies on the widget |
1282 | /// creation order to describe the order of traversal. |
1283 | /// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements |
1284 | /// focus traversal in a direction. |
1285 | /// * [OrderedTraversalPolicy], a policy that describes the order |
1286 | /// explicitly using [FocusTraversalOrder] widgets. |
1287 | class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { |
1288 | /// Constructs a traversal policy that orders the widgets in "reading order". |
1289 | /// |
1290 | /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback} |
1291 | ReadingOrderTraversalPolicy({super.requestFocusCallback}); |
1292 | |
1293 | // Collects the given candidates into groups by directionality. The candidates |
1294 | // have already been sorted as if they all had the directionality of the |
1295 | // nearest Directionality ancestor. |
1296 | List<_ReadingOrderDirectionalGroupData> _collectDirectionalityGroups(Iterable<_ReadingOrderSortData> candidates) { |
1297 | TextDirection? currentDirection = candidates.first.directionality; |
1298 | List<_ReadingOrderSortData> currentGroup = <_ReadingOrderSortData>[]; |
1299 | final List<_ReadingOrderDirectionalGroupData> result = <_ReadingOrderDirectionalGroupData>[]; |
1300 | // Split candidates into runs of the same directionality. |
1301 | for (final _ReadingOrderSortData candidate in candidates) { |
1302 | if (candidate.directionality == currentDirection) { |
1303 | currentGroup.add(candidate); |
1304 | continue; |
1305 | } |
1306 | currentDirection = candidate.directionality; |
1307 | result.add(_ReadingOrderDirectionalGroupData(currentGroup)); |
1308 | currentGroup = <_ReadingOrderSortData>[candidate]; |
1309 | } |
1310 | if (currentGroup.isNotEmpty) { |
1311 | result.add(_ReadingOrderDirectionalGroupData(currentGroup)); |
1312 | } |
1313 | // Sort each group separately. Each group has the same directionality. |
1314 | for (final _ReadingOrderDirectionalGroupData bandGroup in result) { |
1315 | if (bandGroup.members.length == 1) { |
1316 | continue; // No need to sort one node. |
1317 | } |
1318 | _ReadingOrderSortData.sortWithDirectionality(bandGroup.members, bandGroup.directionality!); |
1319 | } |
1320 | return result; |
1321 | } |
1322 | |
1323 | _ReadingOrderSortData _pickNext(List<_ReadingOrderSortData> candidates) { |
1324 | // Find the topmost node by sorting on the top of the rectangles. |
1325 | mergeSort<_ReadingOrderSortData>(candidates, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) => a.rect.top.compareTo(b.rect.top)); |
1326 | final _ReadingOrderSortData topmost = candidates.first; |
1327 | |
1328 | // Find the candidates that are in the same horizontal band as the current one. |
1329 | List<_ReadingOrderSortData> inBand(_ReadingOrderSortData current, Iterable<_ReadingOrderSortData> candidates) { |
1330 | final Rect band = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom); |
1331 | return candidates.where((_ReadingOrderSortData item) { |
1332 | return !item.rect.intersect(band).isEmpty; |
1333 | }).toList(); |
1334 | } |
1335 | |
1336 | final List<_ReadingOrderSortData> inBandOfTop = inBand(topmost, candidates); |
1337 | // It has to have at least topmost in it if the topmost is not degenerate. |
1338 | assert(topmost.rect.isEmpty || inBandOfTop.isNotEmpty); |
1339 | |
1340 | // The topmost rect is in a band by itself, so just return that one. |
1341 | if (inBandOfTop.length <= 1) { |
1342 | return topmost; |
1343 | } |
1344 | |
1345 | // Now that we know there are others in the same band as the topmost, then pick |
1346 | // the one at the beginning, depending on the text direction in force. |
1347 | |
1348 | // Find out the directionality of the nearest common Directionality |
1349 | // ancestor for all nodes. This provides a base directionality to use for |
1350 | // the ordering of the groups. |
1351 | final TextDirection? nearestCommonDirectionality = _ReadingOrderSortData.commonDirectionalityOf(inBandOfTop); |
1352 | |
1353 | // Do an initial common-directionality-based sort to get consistent geometric |
1354 | // ordering for grouping into directionality groups. It has to use the |
1355 | // common directionality to be able to group into sane groups for the |
1356 | // given directionality, since rectangles can overlap and give different |
1357 | // results for different directionalities. |
1358 | _ReadingOrderSortData.sortWithDirectionality(inBandOfTop, nearestCommonDirectionality!); |
1359 | |
1360 | // Collect the top band into internally sorted groups with shared directionality. |
1361 | final List<_ReadingOrderDirectionalGroupData> bandGroups = _collectDirectionalityGroups(inBandOfTop); |
1362 | if (bandGroups.length == 1) { |
1363 | // There's only one directionality group, so just send back the first |
1364 | // one in that group, since it's already sorted. |
1365 | return bandGroups.first.members.first; |
1366 | } |
1367 | |
1368 | // Sort the groups based on the common directionality and bounding boxes. |
1369 | _ReadingOrderDirectionalGroupData.sortWithDirectionality(bandGroups, nearestCommonDirectionality); |
1370 | return bandGroups.first.members.first; |
1371 | } |
1372 | |
1373 | // Sorts the list of nodes based on their geometry into the desired reading |
1374 | // order based on the directionality of the context for each node. |
1375 | @override |
1376 | Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) { |
1377 | if (descendants.length <= 1) { |
1378 | return descendants; |
1379 | } |
1380 | |
1381 | final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[ |
1382 | for (final FocusNode node in descendants) _ReadingOrderSortData(node), |
1383 | ]; |
1384 | |
1385 | final List<FocusNode> sortedList = <FocusNode>[]; |
1386 | final List<_ReadingOrderSortData> unplaced = data; |
1387 | |
1388 | // Pick the initial widget as the one that is at the beginning of the band |
1389 | // of the topmost, or the topmost, if there are no others in its band. |
1390 | _ReadingOrderSortData current = _pickNext(unplaced); |
1391 | sortedList.add(current.node); |
1392 | unplaced.remove(current); |
1393 | |
1394 | // Go through each node, picking the next one after eliminating the previous |
1395 | // one, since removing the previously picked node will expose a new band in |
1396 | // which to choose candidates. |
1397 | while (unplaced.isNotEmpty) { |
1398 | final _ReadingOrderSortData next = _pickNext(unplaced); |
1399 | current = next; |
1400 | sortedList.add(current.node); |
1401 | unplaced.remove(current); |
1402 | } |
1403 | return sortedList; |
1404 | } |
1405 | } |
1406 | |
1407 | /// Base class for all sort orders for [OrderedTraversalPolicy] traversal. |
1408 | /// |
1409 | /// {@template flutter.widgets.FocusOrder.comparable} |
1410 | /// Only orders of the same type are comparable. If a set of widgets in the same |
1411 | /// [FocusTraversalGroup] contains orders that are not comparable with each |
1412 | /// other, it will assert, since the ordering between such keys is undefined. To |
1413 | /// avoid collisions, use a [FocusTraversalGroup] to group similarly ordered |
1414 | /// widgets together. |
1415 | /// |
1416 | /// When overriding, [FocusOrder.doCompare] must be overridden instead of |
1417 | /// [FocusOrder.compareTo], which calls [FocusOrder.doCompare] to do the actual |
1418 | /// comparison. |
1419 | /// {@endtemplate} |
1420 | /// |
1421 | /// See also: |
1422 | /// |
1423 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
1424 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
1425 | /// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree |
1426 | /// for the [OrderedTraversalPolicy] to use. |
1427 | /// * [NumericFocusOrder], for a focus order that describes its order with a |
1428 | /// `double`. |
1429 | /// * [LexicalFocusOrder], a focus order that assigns a string-based lexical |
1430 | /// traversal order to a [FocusTraversalOrder] widget. |
1431 | @immutable |
1432 | abstract class FocusOrder with Diagnosticable implements Comparable<FocusOrder> { |
1433 | /// Abstract const constructor. This constructor enables subclasses to provide |
1434 | /// const constructors so that they can be used in const expressions. |
1435 | const FocusOrder(); |
1436 | |
1437 | /// Compares this object to another [Comparable]. |
1438 | /// |
1439 | /// When overriding [FocusOrder], implement [doCompare] instead of this |
1440 | /// function to do the actual comparison. |
1441 | /// |
1442 | /// Returns a value like a [Comparator] when comparing `this` to [other]. |
1443 | /// That is, it returns a negative integer if `this` is ordered before [other], |
1444 | /// a positive integer if `this` is ordered after [other], |
1445 | /// and zero if `this` and [other] are ordered together. |
1446 | /// |
1447 | /// The [other] argument must be a value that is comparable to this object. |
1448 | @override |
1449 | @nonVirtual |
1450 | int compareTo(FocusOrder other) { |
1451 | assert( |
1452 | runtimeType == other.runtimeType, |
1453 | "The sorting algorithm must not compare incomparable keys, since they don't " |
1454 | 'know how to order themselves relative to each other. Comparing $this with $other' , |
1455 | ); |
1456 | return doCompare(other); |
1457 | } |
1458 | |
1459 | /// The subclass implementation called by [compareTo] to compare orders. |
1460 | /// |
1461 | /// The argument is guaranteed to be of the same [runtimeType] as this object. |
1462 | /// |
1463 | /// The method should return a negative number if this object comes earlier in |
1464 | /// the sort order than the `other` argument; and a positive number if it |
1465 | /// comes later in the sort order than `other`. Returning zero causes the |
1466 | /// system to fall back to the secondary sort order defined by |
1467 | /// [OrderedTraversalPolicy.secondary] |
1468 | @protected |
1469 | int doCompare(covariant FocusOrder other); |
1470 | } |
1471 | |
1472 | /// Can be given to a [FocusTraversalOrder] widget to assign a numerical order |
1473 | /// to a widget subtree that is using a [OrderedTraversalPolicy] to define the |
1474 | /// order in which widgets should be traversed with the keyboard. |
1475 | /// |
1476 | /// {@macro flutter.widgets.FocusOrder.comparable} |
1477 | /// |
1478 | /// See also: |
1479 | /// |
1480 | /// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree |
1481 | /// for the [OrderedTraversalPolicy] to use. |
1482 | class NumericFocusOrder extends FocusOrder { |
1483 | /// Creates an object that describes a focus traversal order numerically. |
1484 | const NumericFocusOrder(this.order); |
1485 | |
1486 | /// The numerical order to assign to the widget subtree using |
1487 | /// [FocusTraversalOrder]. |
1488 | /// |
1489 | /// Determines the placement of this widget in a sequence of widgets that defines |
1490 | /// the order in which this node is traversed by the focus policy. |
1491 | /// |
1492 | /// Lower values will be traversed first. |
1493 | final double order; |
1494 | |
1495 | @override |
1496 | int doCompare(NumericFocusOrder other) => order.compareTo(other.order); |
1497 | |
1498 | @override |
1499 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1500 | super.debugFillProperties(properties); |
1501 | properties.add(DoubleProperty('order' , order)); |
1502 | } |
1503 | } |
1504 | |
1505 | /// Can be given to a [FocusTraversalOrder] widget to use a String to assign a |
1506 | /// lexical order to a widget subtree that is using a |
1507 | /// [OrderedTraversalPolicy] to define the order in which widgets should be |
1508 | /// traversed with the keyboard. |
1509 | /// |
1510 | /// This sorts strings using Dart's default string comparison, which is not |
1511 | /// locale-specific. |
1512 | /// |
1513 | /// {@macro flutter.widgets.FocusOrder.comparable} |
1514 | /// |
1515 | /// See also: |
1516 | /// |
1517 | /// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree |
1518 | /// for the [OrderedTraversalPolicy] to use. |
1519 | class LexicalFocusOrder extends FocusOrder { |
1520 | /// Creates an object that describes a focus traversal order lexically. |
1521 | const LexicalFocusOrder(this.order); |
1522 | |
1523 | /// The String that defines the lexical order to assign to the widget subtree |
1524 | /// using [FocusTraversalOrder]. |
1525 | /// |
1526 | /// Determines the placement of this widget in a sequence of widgets that defines |
1527 | /// the order in which this node is traversed by the focus policy. |
1528 | /// |
1529 | /// Lower lexical values will be traversed first (e.g. 'a' comes before 'z'). |
1530 | final String order; |
1531 | |
1532 | @override |
1533 | int doCompare(LexicalFocusOrder other) => order.compareTo(other.order); |
1534 | |
1535 | @override |
1536 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1537 | super.debugFillProperties(properties); |
1538 | properties.add(StringProperty('order' , order)); |
1539 | } |
1540 | } |
1541 | |
1542 | // Used to help sort the focus nodes in an OrderedFocusTraversalPolicy. |
1543 | class _OrderedFocusInfo { |
1544 | const _OrderedFocusInfo({required this.node, required this.order}); |
1545 | |
1546 | final FocusNode node; |
1547 | final FocusOrder order; |
1548 | } |
1549 | |
1550 | /// A [FocusTraversalPolicy] that orders nodes by an explicit order that resides |
1551 | /// in the nearest [FocusTraversalOrder] widget ancestor. |
1552 | /// |
1553 | /// {@macro flutter.widgets.FocusOrder.comparable} |
1554 | /// |
1555 | /// {@tool dartpad} |
1556 | /// This sample shows how to assign a traversal order to a widget. In the |
1557 | /// example, the focus order goes from bottom right (the "One" button) to top |
1558 | /// left (the "Six" button). |
1559 | /// |
1560 | /// ** See code in examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart ** |
1561 | /// {@end-tool} |
1562 | /// |
1563 | /// See also: |
1564 | /// |
1565 | /// * [FocusTraversalGroup], a widget that groups together and imposes a |
1566 | /// traversal policy on the [Focus] nodes below it in the widget hierarchy. |
1567 | /// * [WidgetOrderTraversalPolicy], a policy that relies on the widget |
1568 | /// creation order to describe the order of traversal. |
1569 | /// * [ReadingOrderTraversalPolicy], a policy that describes the order as the |
1570 | /// natural "reading order" for the current [Directionality]. |
1571 | /// * [NumericFocusOrder], a focus order that assigns a numeric traversal order |
1572 | /// to a [FocusTraversalOrder] widget. |
1573 | /// * [LexicalFocusOrder], a focus order that assigns a string-based lexical |
1574 | /// traversal order to a [FocusTraversalOrder] widget. |
1575 | /// * [FocusOrder], an abstract base class for all types of focus traversal |
1576 | /// orderings. |
1577 | class OrderedTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { |
1578 | /// Constructs a traversal policy that orders widgets for keyboard traversal |
1579 | /// based on an explicit order. |
1580 | /// |
1581 | /// If [secondary] is null, it will default to [ReadingOrderTraversalPolicy]. |
1582 | OrderedTraversalPolicy({this.secondary, super.requestFocusCallback}); |
1583 | |
1584 | /// This is the policy that is used when a node doesn't have an order |
1585 | /// assigned, or when multiple nodes have orders which are identical. |
1586 | /// |
1587 | /// If not set, this defaults to [ReadingOrderTraversalPolicy]. |
1588 | /// |
1589 | /// This policy determines the secondary sorting order of nodes which evaluate |
1590 | /// as having an identical order (including those with no order specified). |
1591 | /// |
1592 | /// Nodes with no order specified will be sorted after nodes with an explicit |
1593 | /// order. |
1594 | final FocusTraversalPolicy? secondary; |
1595 | |
1596 | @override |
1597 | Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) { |
1598 | final FocusTraversalPolicy secondaryPolicy = secondary ?? ReadingOrderTraversalPolicy(); |
1599 | final Iterable<FocusNode> sortedDescendants = secondaryPolicy.sortDescendants(descendants, currentNode); |
1600 | final List<FocusNode> unordered = <FocusNode>[]; |
1601 | final List<_OrderedFocusInfo> ordered = <_OrderedFocusInfo>[]; |
1602 | for (final FocusNode node in sortedDescendants) { |
1603 | final FocusOrder? order = FocusTraversalOrder.maybeOf(node.context!); |
1604 | if (order != null) { |
1605 | ordered.add(_OrderedFocusInfo(node: node, order: order)); |
1606 | } else { |
1607 | unordered.add(node); |
1608 | } |
1609 | } |
1610 | mergeSort<_OrderedFocusInfo>(ordered, compare: (_OrderedFocusInfo a, _OrderedFocusInfo b) { |
1611 | assert( |
1612 | a.order.runtimeType == b.order.runtimeType, |
1613 | 'When sorting nodes for determining focus order, the order ( ${a.order}) of ' |
1614 | "node ${a.node}, isn't the same type as the order ( ${b.order}) of ${b.node}. " |
1615 | "Incompatible order types can't be compared. Use a FocusTraversalGroup to group " |
1616 | 'similar orders together.' , |
1617 | ); |
1618 | return a.order.compareTo(b.order); |
1619 | }); |
1620 | return ordered.map<FocusNode>((_OrderedFocusInfo info) => info.node).followedBy(unordered); |
1621 | } |
1622 | } |
1623 | |
1624 | /// An inherited widget that describes the order in which its child subtree |
1625 | /// should be traversed. |
1626 | /// |
1627 | /// {@macro flutter.widgets.FocusOrder.comparable} |
1628 | /// |
1629 | /// The order for a widget is determined by the [FocusOrder] returned by |
1630 | /// [FocusTraversalOrder.of] for a particular context. |
1631 | class FocusTraversalOrder extends InheritedWidget { |
1632 | /// Creates an inherited widget used to describe the focus order of |
1633 | /// the [child] subtree. |
1634 | const FocusTraversalOrder({super.key, required this.order, required super.child}); |
1635 | |
1636 | /// The order for the widget descendants of this [FocusTraversalOrder]. |
1637 | final FocusOrder order; |
1638 | |
1639 | /// Finds the [FocusOrder] in the nearest ancestor [FocusTraversalOrder] widget. |
1640 | /// |
1641 | /// It does not create a rebuild dependency because changing the traversal |
1642 | /// order doesn't change the widget tree, so nothing needs to be rebuilt as a |
1643 | /// result of an order change. |
1644 | /// |
1645 | /// If no [FocusTraversalOrder] ancestor exists, or the order is null, this |
1646 | /// will assert in debug mode, and throw an exception in release mode. |
1647 | static FocusOrder of(BuildContext context) { |
1648 | final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>(); |
1649 | assert(() { |
1650 | if (marker == null) { |
1651 | throw FlutterError( |
1652 | 'FocusTraversalOrder.of() was called with a context that ' |
1653 | 'does not contain a FocusTraversalOrder widget. No TraversalOrder widget ' |
1654 | 'ancestor could be found starting from the context that was passed to ' |
1655 | 'FocusTraversalOrder.of().\n' |
1656 | 'The context used was:\n' |
1657 | ' $context' , |
1658 | ); |
1659 | } |
1660 | return true; |
1661 | }()); |
1662 | return marker!.order; |
1663 | } |
1664 | |
1665 | /// Finds the [FocusOrder] in the nearest ancestor [FocusTraversalOrder] widget. |
1666 | /// |
1667 | /// It does not create a rebuild dependency because changing the traversal |
1668 | /// order doesn't change the widget tree, so nothing needs to be rebuilt as a |
1669 | /// result of an order change. |
1670 | /// |
1671 | /// If no [FocusTraversalOrder] ancestor exists, or the order is null, returns null. |
1672 | static FocusOrder? maybeOf(BuildContext context) { |
1673 | final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>(); |
1674 | return marker?.order; |
1675 | } |
1676 | |
1677 | // Since the order of traversal doesn't affect display of anything, we don't |
1678 | // need to force a rebuild of anything that depends upon it. |
1679 | @override |
1680 | bool updateShouldNotify(InheritedWidget oldWidget) => false; |
1681 | |
1682 | @override |
1683 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1684 | super.debugFillProperties(properties); |
1685 | properties.add(DiagnosticsProperty<FocusOrder>('order' , order)); |
1686 | } |
1687 | } |
1688 | |
1689 | /// A widget that describes the inherited focus policy for focus traversal for |
1690 | /// its descendants, grouping them into a separate traversal group. |
1691 | /// |
1692 | /// A traversal group is treated as one entity when sorted by the traversal |
1693 | /// algorithm, so it can be used to segregate different parts of the widget tree |
1694 | /// that need to be sorted using different algorithms and/or sort orders when |
1695 | /// using an [OrderedTraversalPolicy]. |
1696 | /// |
1697 | /// Within the group, it will use the given [policy] to order the elements. The |
1698 | /// group itself will be ordered using the parent group's policy. |
1699 | /// |
1700 | /// By default, traverses in reading order using [ReadingOrderTraversalPolicy]. |
1701 | /// |
1702 | /// To prevent the members of the group from being focused, set the |
1703 | /// [descendantsAreFocusable] attribute to false. |
1704 | /// |
1705 | /// {@tool dartpad} |
1706 | /// This sample shows three rows of buttons, each grouped by a |
1707 | /// [FocusTraversalGroup], each with different traversal order policies. Use tab |
1708 | /// traversal to see the order they are traversed in. The first row follows a |
1709 | /// numerical order, the second follows a lexical order (ordered to traverse |
1710 | /// right to left), and the third ignores the numerical order assigned to it and |
1711 | /// traverses in widget order. |
1712 | /// |
1713 | /// ** See code in examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart ** |
1714 | /// {@end-tool} |
1715 | /// |
1716 | /// See also: |
1717 | /// |
1718 | /// * [FocusNode], for a description of the focus system. |
1719 | /// * [WidgetOrderTraversalPolicy], a policy that relies on the widget |
1720 | /// creation order to describe the order of traversal. |
1721 | /// * [ReadingOrderTraversalPolicy], a policy that describes the order as the |
1722 | /// natural "reading order" for the current [Directionality]. |
1723 | /// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements |
1724 | /// focus traversal in a direction. |
1725 | class FocusTraversalGroup extends StatefulWidget { |
1726 | /// Creates a [FocusTraversalGroup] object. |
1727 | FocusTraversalGroup({ |
1728 | super.key, |
1729 | FocusTraversalPolicy? policy, |
1730 | this.descendantsAreFocusable = true, |
1731 | this.descendantsAreTraversable = true, |
1732 | required this.child, |
1733 | }) : policy = policy ?? ReadingOrderTraversalPolicy(); |
1734 | |
1735 | /// The policy used to move the focus from one focus node to another when |
1736 | /// traversing them using a keyboard. |
1737 | /// |
1738 | /// If not specified, traverses in reading order using |
1739 | /// [ReadingOrderTraversalPolicy]. |
1740 | /// |
1741 | /// See also: |
1742 | /// |
1743 | /// * [FocusTraversalPolicy] for the API used to impose traversal order |
1744 | /// policy. |
1745 | /// * [WidgetOrderTraversalPolicy] for a traversal policy that traverses |
1746 | /// nodes in the order they are added to the widget tree. |
1747 | /// * [ReadingOrderTraversalPolicy] for a traversal policy that traverses |
1748 | /// nodes in the reading order defined in the widget tree, and then top to |
1749 | /// bottom. |
1750 | final FocusTraversalPolicy policy; |
1751 | |
1752 | /// {@macro flutter.widgets.Focus.descendantsAreFocusable} |
1753 | final bool descendantsAreFocusable; |
1754 | |
1755 | /// {@macro flutter.widgets.Focus.descendantsAreTraversable} |
1756 | final bool descendantsAreTraversable; |
1757 | |
1758 | /// The child widget of this [FocusTraversalGroup]. |
1759 | /// |
1760 | /// {@macro flutter.widgets.ProxyWidget.child} |
1761 | final Widget child; |
1762 | |
1763 | /// Returns the [FocusTraversalPolicy] that applies to the nearest ancestor of |
1764 | /// the given [FocusNode]. |
1765 | /// |
1766 | /// Will return null if no [FocusTraversalPolicy] ancestor applies to the |
1767 | /// given [FocusNode]. |
1768 | /// |
1769 | /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup] |
1770 | /// into the widget tree, which will associate a policy with the focus tree |
1771 | /// under the nearest ancestor [Focus] widget. |
1772 | /// |
1773 | /// This function differs from [maybeOf] in that it takes a [FocusNode] and |
1774 | /// only traverses the focus tree to determine the policy in effect. Unlike |
1775 | /// this function, the [maybeOf] function takes a [BuildContext] and first |
1776 | /// walks up the widget tree to find the nearest ancestor [Focus] or |
1777 | /// [FocusScope] widget, and then calls this function with the focus node |
1778 | /// associated with that widget to determine the policy in effect. |
1779 | static FocusTraversalPolicy? maybeOfNode(FocusNode node) { |
1780 | return _getGroupNode(node)?.policy; |
1781 | } |
1782 | |
1783 | static _FocusTraversalGroupNode? _getGroupNode(FocusNode node) { |
1784 | while (node.parent != null) { |
1785 | if (node.context == null) { |
1786 | return null; |
1787 | } |
1788 | if (node is _FocusTraversalGroupNode) { |
1789 | return node; |
1790 | } |
1791 | node = node.parent!; |
1792 | } |
1793 | return null; |
1794 | } |
1795 | |
1796 | /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the |
1797 | /// nearest ancestor [Focus] widget, given a [BuildContext]. |
1798 | /// |
1799 | /// Will throw a [FlutterError] in debug mode, and throw a null check |
1800 | /// exception in release mode, if no [Focus] ancestor is found, or if no |
1801 | /// [FocusTraversalPolicy] applies to the associated [FocusNode]. |
1802 | /// |
1803 | /// {@template flutter.widgets.focus_traversal.FocusTraversalGroup.of} |
1804 | /// This function looks up the nearest ancestor [Focus] (or [FocusScope]) |
1805 | /// widget, and uses its [FocusNode] (or [FocusScopeNode]) to walk up the |
1806 | /// focus tree to find the applicable [FocusTraversalPolicy] for that node. |
1807 | /// |
1808 | /// Calling this function does not create a rebuild dependency because |
1809 | /// changing the traversal order doesn't change the widget tree, so nothing |
1810 | /// needs to be rebuilt as a result of an order change. |
1811 | /// |
1812 | /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup] |
1813 | /// into the widget tree, which will associate a policy with the focus tree |
1814 | /// under the nearest ancestor [Focus] widget. |
1815 | /// {@endtemplate} |
1816 | /// |
1817 | /// See also: |
1818 | /// |
1819 | /// * [maybeOf] for a similar function that will return null if no |
1820 | /// [FocusTraversalGroup] ancestor is found. |
1821 | /// * [maybeOfNode] for a function that will look for a policy using a given |
1822 | /// [FocusNode], and return null if no policy applies. |
1823 | static FocusTraversalPolicy of(BuildContext context) { |
1824 | final FocusTraversalPolicy? policy = maybeOf(context); |
1825 | assert(() { |
1826 | if (policy == null) { |
1827 | throw FlutterError( |
1828 | 'Unable to find a Focus or FocusScope widget in the given context, or the FocusNode ' |
1829 | 'from with the widget that was found is not associated with a FocusTraversalPolicy.\n' |
1830 | 'FocusTraversalGroup.of() was called with a context that does not contain a ' |
1831 | 'Focus or FocusScope widget, or there was no FocusTraversalPolicy in effect.\n' |
1832 | 'This can happen if there is not a FocusTraversalGroup that defines the policy, ' |
1833 | 'or if the context comes from a widget that is above the WidgetsApp, MaterialApp, ' |
1834 | 'or CupertinoApp widget (those widgets introduce an implicit default policy) \n' |
1835 | 'The context used was:\n' |
1836 | ' $context' , |
1837 | ); |
1838 | } |
1839 | return true; |
1840 | }()); |
1841 | return policy!; |
1842 | } |
1843 | |
1844 | /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the |
1845 | /// nearest ancestor [Focus] widget, or null, given a [BuildContext]. |
1846 | /// |
1847 | /// Will return null if it doesn't find an ancestor [Focus] or [FocusScope] |
1848 | /// widget, or doesn't find a [FocusTraversalPolicy] that applies to the node. |
1849 | /// |
1850 | /// {@macro flutter.widgets.focus_traversal.FocusTraversalGroup.of} |
1851 | /// |
1852 | /// See also: |
1853 | /// |
1854 | /// * [maybeOfNode] for a similar function that will look for a policy using a |
1855 | /// given [FocusNode]. |
1856 | /// * [of] for a similar function that will throw if no [FocusTraversalPolicy] |
1857 | /// applies. |
1858 | static FocusTraversalPolicy? maybeOf(BuildContext context) { |
1859 | final FocusNode? node = Focus.maybeOf(context, scopeOk: true, createDependency: false); |
1860 | if (node == null) { |
1861 | return null; |
1862 | } |
1863 | return FocusTraversalGroup.maybeOfNode(node); |
1864 | } |
1865 | |
1866 | @override |
1867 | State<FocusTraversalGroup> createState() => _FocusTraversalGroupState(); |
1868 | |
1869 | @override |
1870 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
1871 | super.debugFillProperties(properties); |
1872 | properties.add(DiagnosticsProperty<FocusTraversalPolicy>('policy' , policy)); |
1873 | } |
1874 | } |
1875 | |
1876 | // A special focus node subclass that only FocusTraversalGroup uses so that it |
1877 | // can be used to cache the policy in the focus tree, and so that the traversal |
1878 | // code can find groups in the focus tree. |
1879 | class _FocusTraversalGroupNode extends FocusNode { |
1880 | _FocusTraversalGroupNode({ |
1881 | super.debugLabel, |
1882 | required this.policy, |
1883 | }) { |
1884 | if (kFlutterMemoryAllocationsEnabled) { |
1885 | ChangeNotifier.maybeDispatchObjectCreation(this); |
1886 | } |
1887 | } |
1888 | |
1889 | FocusTraversalPolicy policy; |
1890 | } |
1891 | |
1892 | class _FocusTraversalGroupState extends State<FocusTraversalGroup> { |
1893 | // The internal focus node used to collect the children of this node into a |
1894 | // group, and to provide a context for the traversal algorithm to sort the |
1895 | // group with. It's a special subclass of FocusNode just so that it can be |
1896 | // identified when walking the focus tree during traversal, and hold the |
1897 | // current policy. |
1898 | late final _FocusTraversalGroupNode focusNode = _FocusTraversalGroupNode( |
1899 | debugLabel: 'FocusTraversalGroup' , |
1900 | policy: widget.policy, |
1901 | ); |
1902 | |
1903 | @override |
1904 | void dispose() { |
1905 | focusNode.dispose(); |
1906 | super.dispose(); |
1907 | } |
1908 | |
1909 | @override |
1910 | void didUpdateWidget (FocusTraversalGroup oldWidget) { |
1911 | super.didUpdateWidget(oldWidget); |
1912 | if (oldWidget.policy != widget.policy) { |
1913 | focusNode.policy = widget.policy; |
1914 | } |
1915 | } |
1916 | |
1917 | @override |
1918 | Widget build(BuildContext context) { |
1919 | return Focus( |
1920 | focusNode: focusNode, |
1921 | canRequestFocus: false, |
1922 | skipTraversal: true, |
1923 | includeSemantics: false, |
1924 | descendantsAreFocusable: widget.descendantsAreFocusable, |
1925 | descendantsAreTraversable: widget.descendantsAreTraversable, |
1926 | child: widget.child, |
1927 | ); |
1928 | } |
1929 | } |
1930 | |
1931 | /// An intent for use with the [RequestFocusAction], which supplies the |
1932 | /// [FocusNode] that should be focused. |
1933 | class RequestFocusIntent extends Intent { |
1934 | /// Creates an intent used with [RequestFocusAction]. |
1935 | /// |
1936 | /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback} |
1937 | const RequestFocusIntent(this.focusNode, { |
1938 | TraversalRequestFocusCallback? requestFocusCallback |
1939 | }) : requestFocusCallback = requestFocusCallback ?? FocusTraversalPolicy.defaultTraversalRequestFocusCallback; |
1940 | |
1941 | /// The callback used to move the focus to the node [focusNode]. |
1942 | /// By default it requests focus on the node and ensures the node is visible |
1943 | /// if it's in a scrollable. |
1944 | final TraversalRequestFocusCallback requestFocusCallback; |
1945 | |
1946 | /// The [FocusNode] that is to be focused. |
1947 | final FocusNode focusNode; |
1948 | } |
1949 | |
1950 | /// An [Action] that requests the focus on the node it is given in its |
1951 | /// [RequestFocusIntent]. |
1952 | /// |
1953 | /// This action can be used to request focus for a particular node, by calling |
1954 | /// [Action.invoke] like so: |
1955 | /// |
1956 | /// ```dart |
1957 | /// Actions.invoke(context, RequestFocusIntent(focusNode)); |
1958 | /// ``` |
1959 | /// |
1960 | /// Where the `focusNode` is the node for which the focus will be requested. |
1961 | /// |
1962 | /// The difference between requesting focus in this way versus calling |
1963 | /// [FocusNode.requestFocus] directly is that it will use the [Action] |
1964 | /// registered in the nearest [Actions] widget associated with |
1965 | /// [RequestFocusIntent] to make the request, rather than just requesting focus |
1966 | /// directly. This allows the action to have additional side effects, like |
1967 | /// logging, or undo and redo functionality. |
1968 | /// |
1969 | /// This [RequestFocusAction] class is the default action associated with the |
1970 | /// [RequestFocusIntent] in the [WidgetsApp]. It requests focus. You |
1971 | /// can redefine the associated action with your own [Actions] widget. |
1972 | /// |
1973 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
1974 | class RequestFocusAction extends Action<RequestFocusIntent> { |
1975 | |
1976 | @override |
1977 | void invoke(RequestFocusIntent intent) { |
1978 | intent.requestFocusCallback(intent.focusNode); |
1979 | } |
1980 | } |
1981 | |
1982 | /// An [Intent] bound to [NextFocusAction], which moves the focus to the next |
1983 | /// focusable node in the focus traversal order. |
1984 | /// |
1985 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
1986 | class NextFocusIntent extends Intent { |
1987 | /// Creates an intent that is used with [NextFocusAction]. |
1988 | const NextFocusIntent(); |
1989 | } |
1990 | |
1991 | /// An [Action] that moves the focus to the next focusable node in the focus |
1992 | /// order. |
1993 | /// |
1994 | /// This action is the default action registered for the [NextFocusIntent], and |
1995 | /// by default is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp]. |
1996 | /// |
1997 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
1998 | class NextFocusAction extends Action<NextFocusIntent> { |
1999 | /// Attempts to pass the focus to the next widget. |
2000 | /// |
2001 | /// Returns true if a widget was focused as a result of invoking this action. |
2002 | /// |
2003 | /// Returns false when the traversal reached the end and the engine must pass |
2004 | /// focus to platform UI. |
2005 | @override |
2006 | bool invoke(NextFocusIntent intent) { |
2007 | return primaryFocus!.nextFocus(); |
2008 | } |
2009 | |
2010 | @override |
2011 | KeyEventResult toKeyEventResult(NextFocusIntent intent, bool invokeResult) { |
2012 | return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers; |
2013 | } |
2014 | } |
2015 | |
2016 | /// An [Intent] bound to [PreviousFocusAction], which moves the focus to the |
2017 | /// previous focusable node in the focus traversal order. |
2018 | /// |
2019 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
2020 | class PreviousFocusIntent extends Intent { |
2021 | /// Creates an intent that is used with [PreviousFocusAction]. |
2022 | const PreviousFocusIntent(); |
2023 | } |
2024 | |
2025 | /// An [Action] that moves the focus to the previous focusable node in the focus |
2026 | /// order. |
2027 | /// |
2028 | /// This action is the default action registered for the [PreviousFocusIntent], |
2029 | /// and by default is bound to a combination of the [LogicalKeyboardKey.tab] key |
2030 | /// and the [LogicalKeyboardKey.shift] key in the [WidgetsApp]. |
2031 | /// |
2032 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
2033 | class PreviousFocusAction extends Action<PreviousFocusIntent> { |
2034 | /// Attempts to pass the focus to the previous widget. |
2035 | /// |
2036 | /// Returns true if a widget was focused as a result of invoking this action. |
2037 | /// |
2038 | /// Returns false when the traversal reached the beginning and the engine must |
2039 | /// pass focus to platform UI. |
2040 | @override |
2041 | bool invoke(PreviousFocusIntent intent) { |
2042 | return primaryFocus!.previousFocus(); |
2043 | } |
2044 | |
2045 | @override |
2046 | KeyEventResult toKeyEventResult(PreviousFocusIntent intent, bool invokeResult) { |
2047 | return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers; |
2048 | } |
2049 | } |
2050 | |
2051 | /// An [Intent] that represents moving to the next focusable node in the given |
2052 | /// [direction]. |
2053 | /// |
2054 | /// This is the [Intent] bound by default to the [LogicalKeyboardKey.arrowUp], |
2055 | /// [LogicalKeyboardKey.arrowDown], [LogicalKeyboardKey.arrowLeft], and |
2056 | /// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the |
2057 | /// appropriate associated directions. |
2058 | /// |
2059 | /// See [FocusTraversalPolicy] for more information about focus traversal. |
2060 | class DirectionalFocusIntent extends Intent { |
2061 | /// Creates an intent used to move the focus in the given [direction]. |
2062 | const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true}); |
2063 | |
2064 | /// The direction in which to look for the next focusable node when the |
2065 | /// associated [DirectionalFocusAction] is invoked. |
2066 | final TraversalDirection direction; |
2067 | |
2068 | /// If true, then directional focus actions that occur within a text field |
2069 | /// will not happen when the focus node which received the key is a text |
2070 | /// field. |
2071 | /// |
2072 | /// Defaults to true. |
2073 | final bool ignoreTextFields; |
2074 | |
2075 | @override |
2076 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
2077 | super.debugFillProperties(properties); |
2078 | properties.add(EnumProperty<TraversalDirection>('direction' , direction)); |
2079 | } |
2080 | } |
2081 | |
2082 | /// An [Action] that moves the focus to the focusable node in the direction |
2083 | /// configured by the associated [DirectionalFocusIntent.direction]. |
2084 | /// |
2085 | /// This is the [Action] associated with [DirectionalFocusIntent] and bound by |
2086 | /// default to the [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown], |
2087 | /// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in |
2088 | /// the [WidgetsApp], with the appropriate associated directions. |
2089 | class DirectionalFocusAction extends Action<DirectionalFocusIntent> { |
2090 | /// Creates a [DirectionalFocusAction]. |
2091 | DirectionalFocusAction() : _isForTextField = false; |
2092 | |
2093 | /// Creates a [DirectionalFocusAction] that ignores [DirectionalFocusIntent]s |
2094 | /// whose `ignoreTextFields` field is true. |
2095 | DirectionalFocusAction.forTextField() : _isForTextField = true; |
2096 | |
2097 | // Whether this action is defined in a text field. |
2098 | final bool _isForTextField; |
2099 | @override |
2100 | void invoke(DirectionalFocusIntent intent) { |
2101 | if (!intent.ignoreTextFields || !_isForTextField) { |
2102 | primaryFocus!.focusInDirection(intent.direction); |
2103 | } |
2104 | } |
2105 | } |
2106 | |
2107 | /// A widget that controls whether or not the descendants of this widget are |
2108 | /// traversable. |
2109 | /// |
2110 | /// Does not affect the value of [FocusNode.skipTraversal] of the descendants. |
2111 | /// |
2112 | /// See also: |
2113 | /// |
2114 | /// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree. |
2115 | /// * [ExcludeFocus], a widget that excludes its descendants from focusability. |
2116 | /// * [FocusTraversalGroup], a widget that groups widgets for focus traversal, |
2117 | /// and can also be used in the same way as this widget by setting its |
2118 | /// `descendantsAreFocusable` attribute. |
2119 | class ExcludeFocusTraversal extends StatelessWidget { |
2120 | /// Const constructor for [ExcludeFocusTraversal] widget. |
2121 | const ExcludeFocusTraversal({ |
2122 | super.key, |
2123 | this.excluding = true, |
2124 | required this.child, |
2125 | }); |
2126 | |
2127 | /// If true, will make this widget's descendants untraversable. |
2128 | /// |
2129 | /// Defaults to true. |
2130 | /// |
2131 | /// Does not affect the value of [FocusNode.skipTraversal] on the descendants. |
2132 | /// |
2133 | /// See also: |
2134 | /// |
2135 | /// * [Focus.descendantsAreTraversable], the attribute of a [Focus] widget that |
2136 | /// controls this same property for focus widgets. |
2137 | /// * [FocusTraversalGroup], a widget used to group together and configure the |
2138 | /// focus traversal policy for a widget subtree that has a |
2139 | /// `descendantsAreFocusable` parameter to conditionally block focus for a |
2140 | /// subtree. |
2141 | final bool excluding; |
2142 | |
2143 | /// The child widget of this [ExcludeFocusTraversal]. |
2144 | /// |
2145 | /// {@macro flutter.widgets.ProxyWidget.child} |
2146 | final Widget child; |
2147 | |
2148 | @override |
2149 | Widget build(BuildContext context) { |
2150 | return Focus( |
2151 | canRequestFocus: false, |
2152 | skipTraversal: true, |
2153 | includeSemantics: false, |
2154 | descendantsAreTraversable: !excluding, |
2155 | child: child, |
2156 | ); |
2157 | } |
2158 | } |
2159 | |