Unverified Commit 14309b93 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adds the semantic node traversal API. (#14060)

This adds an API for defining the semantic node traversal order.

It adds a sortOrder argument to the Semantics widget, which is a class that can define a list of sort keys to sort on. The keys are sorted globally so that an order that doesn't have to do with the current widget hierarchy may be defined.

It also adds a shortcut sortKey argument to the Semantics widget that simply sets the sortOrder to just contain that key.

The platform side (flutter/engine#4540) gets an additional member in the SemanticsData object that is an integer describing where in the overall order each semantics node belongs. There is an associated engine-side change that takes this integer and uses it to order widgets for the platform's accessibility services.
parent 0f7d4428
......@@ -130,7 +130,7 @@ class StockHomeState extends State<StockHome> {
debugDumpApp();
debugDumpRenderTree();
debugDumpLayerTree();
debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversal);
debugDumpSemanticsTree(DebugSemanticsDumpOrder.geometricOrder);
} catch (e, stack) {
debugPrint('Exception while dumping app:\n$e\n$stack');
}
......
......@@ -101,8 +101,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
);
registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInTraversalOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversal); return debugPrintDone; }
name: 'debugDumpSemanticsTreeInGeometricOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.geometricOrder); return debugPrintDone; }
);
registerSignalServiceExtension(
......
......@@ -2228,7 +2228,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// Dirty the semantics tree starting at `this` until we have reached a
// RenderObject that is a semantics boundary. All semantics past this
// RenderObject are still up-to date. Therefore, we will later only rebuild
// the semantics subtree starting at th identified semantics boundary.
// the semantics subtree starting at the identified semantics boundary.
final bool wasSemanticsBoundary = _semantics != null && _cachedSemanticsConfiguration?.isSemanticBoundary == true;
_cachedSemanticsConfiguration = null;
......@@ -2254,7 +2254,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// remove it as it is no longer guaranteed that its semantics
// node will continue to be in the tree. If it still is in the tree, the
// ancestor `node` added to [owner._nodesNeedingSemantics] at the end of
// this block will ensure that the semantics of `this` node actually get
// this block will ensure that the semantics of `this` node actually gets
// updated.
// (See semantics_10_test.dart for an example why this is required).
owner._nodesNeedingSemantics.remove(this);
......@@ -3005,8 +3005,8 @@ class _ContainerSemanticsFragment extends _SemanticsFragment {
/// A [_SemanticsFragment] that describes which concrete semantic information
/// a [RenderObject] wants to add to the [SemanticsNode] of its parent.
///
/// Specifically, it describes what children (as returned by [compileChildren])
/// should be added to the parent's [SemanticsNode] and what [config] should be
/// Specifically, it describes which children (as returned by [compileChildren])
/// should be added to the parent's [SemanticsNode] and which [config] should be
/// merged into the parent's [SemanticsNode].
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({
......@@ -3082,7 +3082,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
/// An [_InterestingSemanticsFragment] that produces the root [SemanticsNode] of
/// the semantics tree.
///
/// The root node is available as only element in the Iterable returned by
/// The root node is available as the only element in the Iterable returned by
/// [children].
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({
......@@ -3144,7 +3144,7 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
/// fragment it will create a new [SemanticsNode]. The newly created node will
/// be annotated with the [SemanticsConfiguration] that - without the call to
/// [markAsExplicit] - would have been merged into the parent's [SemanticsNode].
/// Similarity, the new node will also take over the children that otherwise
/// Similarly, the new node will also take over the children that otherwise
/// would have been added to the parent's [SemanticsNode].
///
/// After a call to [markAsExplicit] the only element returned by [children]
......
......@@ -3008,6 +3008,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
String decreasedValue,
String hint,
TextDirection textDirection,
SemanticsSortOrder sortOrder,
VoidCallback onTap,
VoidCallback onLongPress,
VoidCallback onScrollLeft,
......@@ -3035,6 +3036,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_decreasedValue = decreasedValue,
_hint = hint,
_textDirection = textDirection,
_sortOrder = sortOrder,
_onTap = onTap,
_onLongPress = onLongPress,
_onScrollLeft = onScrollLeft,
......@@ -3208,6 +3210,20 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// Sets the [SemanticsNode.sortOrder] to the given value.
///
/// This defines how this node will be sorted with the other semantics nodes
/// to determine the order in which they are traversed by the accessibility
/// services on the platform (e.g. VoiceOver on iOS and TalkBack on Android).
SemanticsSortOrder get sortOrder => _sortOrder;
SemanticsSortOrder _sortOrder;
set sortOrder(SemanticsSortOrder value) {
if (sortOrder == value)
return;
_sortOrder = value;
markNeedsSemanticsUpdate();
}
/// The handler for [SemanticsAction.tap].
///
/// This is the semantic equivalent of a user briefly tapping the screen with
......@@ -3504,6 +3520,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.hint = hint;
if (textDirection != null)
config.textDirection = textDirection;
if (sortOrder != null)
config.sortOrder = sortOrder;
// Registering _perform* as action handlers instead of the user provided
// ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update.
......
......@@ -12,7 +12,6 @@ import 'package:flutter/painting.dart' show MatrixUtils, TransformProperty;
import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
import 'semantics_event.dart';
export 'dart:ui' show SemanticsAction;
......@@ -93,6 +92,7 @@ class SemanticsData extends Diagnosticable {
@required this.decreasedValue,
@required this.hint,
@required this.textDirection,
@required this.nextNodeId,
@required this.rect,
@required this.textSelection,
this.tags,
......@@ -148,6 +148,10 @@ class SemanticsData extends Diagnosticable {
/// [increasedValue], and [decreasedValue].
final TextDirection textDirection;
/// The index indicating the ID of the next node in the traversal order after
/// this node for the platform's accessibility services.
final int nextNodeId;
/// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field.
final TextSelection textSelection;
......@@ -198,6 +202,7 @@ class SemanticsData extends Diagnosticable {
properties.add(new StringProperty('decreasedValue', decreasedValue, defaultValue: ''));
properties.add(new StringProperty('hint', hint, defaultValue: ''));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new IntProperty('nextNodeId', nextNodeId, defaultValue: null));
if (textSelection?.isValid == true)
properties.add(new MessageProperty('text selection', '[${textSelection.start}, ${textSelection.end}]'));
}
......@@ -215,6 +220,7 @@ class SemanticsData extends Diagnosticable {
&& typedOther.decreasedValue == decreasedValue
&& typedOther.hint == hint
&& typedOther.textDirection == textDirection
&& typedOther.nextNodeId == nextNodeId
&& typedOther.rect == rect
&& setEquals(typedOther.tags, tags)
&& typedOther.textSelection == textSelection
......@@ -222,7 +228,7 @@ class SemanticsData extends Diagnosticable {
}
@override
int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, rect, tags, textSelection, transform);
int get hashCode => ui.hashValues(flags, actions, label, value, increasedValue, decreasedValue, hint, textDirection, nextNodeId, rect, tags, textSelection, transform);
}
class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
......@@ -256,8 +262,6 @@ class _SemanticsDiagnosticableNode extends DiagnosticableNode<SemanticsNode> {
@immutable
class SemanticsProperties extends DiagnosticableTree {
/// Creates a semantic annotation.
///
/// The [container] argument must not be null.
const SemanticsProperties({
this.enabled,
this.checked,
......@@ -269,6 +273,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.decreasedValue,
this.hint,
this.textDirection,
this.sortOrder,
this.onTap,
this.onLongPress,
this.onScrollLeft,
......@@ -377,6 +382,23 @@ class SemanticsProperties extends DiagnosticableTree {
/// Defaults to the ambient [Directionality].
final TextDirection textDirection;
/// Provides a traversal sorting order for this [Semantics] node.
///
/// This is used to describe the order in which the semantic node should be
/// traversed by the accessibility services on the platform (e.g. VoiceOver
/// on iOS and TalkBack on Android).
///
/// If [sortOrder.discardParentOrder] is false (the default), [sortOrder]'s
/// sort keys are appended to the list of keys from any ancestor nodes into a
/// list of [SemanticsSortKey]s that are compared in pairwise order.
/// Otherwise, it ignores the ancestor's [sortOrder] on this node.
///
/// See also:
///
/// * [SemanticsSortOrder] which provides a way to specify the order in
/// which semantic nodes are sorted.
final SemanticsSortOrder sortOrder;
/// The handler for [SemanticsAction.tap].
///
/// This is the semantic equivalent of a user briefly tapping the screen with
......@@ -536,6 +558,7 @@ class SemanticsProperties extends DiagnosticableTree {
description.add(new StringProperty('value', value));
description.add(new StringProperty('hint', hint));
description.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
description.add(new DiagnosticsProperty<SemanticsSortOrder>('sortOrder', sortOrder, defaultValue: null));
}
}
......@@ -732,7 +755,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
}
if (newChildren != null) {
for (SemanticsNode child in newChildren) {
assert(!child.isInvisible, '$child is invisible and should not be added as child of $this.');
assert(!child.isInvisible, 'Child $child is invisible and should not be added as a child of $this.');
child._dead = false;
}
}
......@@ -890,6 +913,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_increasedValue != config.increasedValue ||
_flags != config._flags ||
_textDirection != config.textDirection ||
_sortOrder != config._sortOrder ||
_textSelection != config._textSelection ||
_actionsAsBits != config._actionsAsBits ||
_mergeAllDescendantsIntoThisNode != config.isMergingSemanticsOfDescendants;
......@@ -957,6 +981,31 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
TextDirection get textDirection => _textDirection;
TextDirection _textDirection = _kEmptyConfig.textDirection;
/// The sort order for ordering the traversal of [SemanticsNode]s by the
/// platform's accessibility services (e.g. VoiceOver on iOS and TalkBack on
/// Android). This is used to determine the [nextNodeId] during a semantics update.
SemanticsSortOrder _sortOrder;
SemanticsSortOrder get sortOrder => _sortOrder;
/// The ID of the next node in the traversal order after this node.
///
/// Only valid after at least one semantics update has been built.
///
/// This is the value passed to the engine to tell it what the order
/// should be for traversing semantics nodes.
///
/// If this is set to -1, it will indicate that there is no next node to
/// the engine (i.e. this is the last node in the sort order). When it is
/// null, it means that no semantics update has been built yet.
int _nextNodeId;
void _updateNextNodeId(int value) {
if (value == _nextNodeId)
return;
_nextNodeId = value;
_markDirty();
}
int get nextNodeId => _nextNodeId;
/// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field.
TextSelection get textSelection => _textSelection;
......@@ -990,6 +1039,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
_hint = config.hint;
_flags = config._flags;
_textDirection = config.textDirection;
_sortOrder = config.sortOrder;
_actions = new Map<SemanticsAction, _SemanticsActionHandler>.from(config._actions);
_actionsAsBits = config._actionsAsBits;
_textSelection = config._textSelection;
......@@ -1021,6 +1071,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
String increasedValue = _increasedValue;
String decreasedValue = _decreasedValue;
TextDirection textDirection = _textDirection;
int nextNodeId = _nextNodeId;
Set<SemanticsTag> mergedTags = tags == null ? null : new Set<SemanticsTag>.from(tags);
TextSelection textSelection = _textSelection;
......@@ -1030,6 +1081,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
flags |= node._flags;
actions |= node._actionsAsBits;
textDirection ??= node._textDirection;
nextNodeId ??= node._nextNodeId;
textSelection ??= node._textSelection;
if (value == '' || value == null)
value = node._value;
......@@ -1066,6 +1118,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
decreasedValue: decreasedValue,
hint: hint,
textDirection: textDirection,
nextNodeId: nextNodeId,
rect: rect,
transform: transform,
tags: mergedTags,
......@@ -1089,9 +1142,10 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
} else {
final int childCount = _children.length;
children = new Int32List(childCount);
for (int i = 0; i < childCount; ++i)
for (int i = 0; i < childCount; ++i) {
children[i] = _children[i].id;
}
}
builder.updateNode(
id: id,
flags: data.flags,
......@@ -1103,6 +1157,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
increasedValue: data.increasedValue,
hint: data.hint,
textDirection: data.textDirection,
nextNodeId: data.nextNodeId,
textSelectionBase: data.textSelection != null ? data.textSelection.baseOffset : -1,
textSelectionExtent: data.textSelection != null ? data.textSelection.extentOffset : -1,
transform: data.transform?.storage ?? _kIdentityTransform,
......@@ -1166,12 +1221,15 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
properties.add(new FlagProperty('isFocused', value: _hasFlag(SemanticsFlag.isFocused), ifTrue: 'focused'));
properties.add(new FlagProperty('isButton', value: _hasFlag(SemanticsFlag.isButton), ifTrue: 'button'));
properties.add(new FlagProperty('isTextField', value: _hasFlag(SemanticsFlag.isTextField), ifTrue: 'textField'));
properties.add(new FlagProperty('isInvisible', value: isInvisible, ifTrue: 'invisible'));
properties.add(new StringProperty('label', _label, defaultValue: ''));
properties.add(new StringProperty('value', _value, defaultValue: ''));
properties.add(new StringProperty('increasedValue', _increasedValue, defaultValue: ''));
properties.add(new StringProperty('decreasedValue', _decreasedValue, defaultValue: ''));
properties.add(new StringProperty('hint', _hint, defaultValue: ''));
properties.add(new EnumProperty<TextDirection>('textDirection', _textDirection, defaultValue: null));
properties.add(new IntProperty('nextNodeId', _nextNodeId, defaultValue: null));
properties.add(new DiagnosticsProperty<SemanticsSortOrder>('sortOrder', sortOrder, defaultValue: null));
if (_textSelection?.isValid == true)
properties.add(new MessageProperty('text selection', '[${_textSelection.start}, ${_textSelection.end}]'));
}
......@@ -1185,7 +1243,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
String prefixLineOne: '',
String prefixOtherLines,
DiagnosticLevel minLevel: DiagnosticLevel.debug,
DebugSemanticsDumpOrder childOrder: DebugSemanticsDumpOrder.traversal,
DebugSemanticsDumpOrder childOrder: DebugSemanticsDumpOrder.geometricOrder,
}) {
assert(childOrder != null);
return toDiagnosticsNode(childOrder: childOrder).toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
......@@ -1195,7 +1253,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
DiagnosticsNode toDiagnosticsNode({
String name,
DiagnosticsTreeStyle style: DiagnosticsTreeStyle.dense,
DebugSemanticsDumpOrder childOrder: DebugSemanticsDumpOrder.traversal,
DebugSemanticsDumpOrder childOrder: DebugSemanticsDumpOrder.geometricOrder,
}) {
return new _SemanticsDiagnosticableNode(
name: name,
......@@ -1218,7 +1276,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
return const <SemanticsNode>[];
switch (childOrder) {
case DebugSemanticsDumpOrder.traversal:
case DebugSemanticsDumpOrder.geometricOrder:
return new List<SemanticsNode>.from(_children)..sort(_geometryComparator);
case DebugSemanticsDumpOrder.inverseHitTest:
return _children;
......@@ -1235,6 +1293,34 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin {
}
}
/// A helper class to contain a semantics node and the effective
/// sort order of that node during the traversal of the tree. This is
/// what is actually sorted when semantics nodes are sorted.
class _TraversalSortNode implements Comparable<_TraversalSortNode> {
_TraversalSortNode(this.node, this.order);
SemanticsNode node;
SemanticsSortOrder order;
int _compareGeometry(_TraversalSortNode other) {
// TODO(gspencer): Move the geometric comparison from the platform side to here.
// This involves calculating the globally-transformed quad for the semantics node rect
// and then sorting by its bounding box, based on the container's directionality.
return 0;
}
@override
int compareTo(_TraversalSortNode other) {
if (order == null || other?.order == null) {
return _compareGeometry(other);
}
final int comparison = order.compareTo(other.order);
if (comparison != 0) {
return comparison;
}
return _compareGeometry(other);
}
}
/// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics.
///
......@@ -1259,10 +1345,41 @@ class SemanticsOwner extends ChangeNotifier {
super.dispose();
}
// Updates the nextNodeId IDs on the semantics nodes. These IDs are used
// on the platform side to order the nodes for traversal by the accessibility
// services. If the nextNodeId for a node changes, the node will be marked as
// dirty.
void _updateTraversalOrder() {
final List<_TraversalSortNode> nodesInSemanticsTraversalOrder = <_TraversalSortNode>[];
SemanticsSortOrder currentSortOrder = new SemanticsSortOrder(keys: <SemanticsSortKey>[]);
bool visitor(SemanticsNode node) {
final SemanticsSortOrder previousOrder = currentSortOrder;
if (node.sortOrder != null) {
currentSortOrder = currentSortOrder.merge(node.sortOrder);
}
final _TraversalSortNode traversalNode = new _TraversalSortNode(node, currentSortOrder);
nodesInSemanticsTraversalOrder.add(traversalNode);
if (node.hasChildren) {
node.visitChildren(visitor);
}
currentSortOrder = previousOrder;
return true;
}
rootSemanticsNode.visitChildren(visitor);
nodesInSemanticsTraversalOrder.sort();
int nextNodeId = -1;
for (_TraversalSortNode node in nodesInSemanticsTraversalOrder.reversed) {
node.node._updateNextNodeId(nextNodeId);
nextNodeId = node.node.id;
}
}
/// Update the semantics using [Window.updateSemantics].
void sendSemanticsUpdate() {
if (_dirtyNodes.isEmpty)
return;
// Nodes that change their nextNodeId will be marked as dirty.
_updateTraversalOrder();
final List<SemanticsNode> visitedNodes = <SemanticsNode>[];
while (_dirtyNodes.isNotEmpty) {
final List<SemanticsNode> localDirtyNodes = _dirtyNodes.where((SemanticsNode node) => !_detachedNodes.contains(node)).toList();
......@@ -1770,6 +1887,22 @@ class SemanticsConfiguration {
/// * [addAction] to add an action.
_SemanticsActionHandler getActionHandler(SemanticsAction action) => _actions[action];
/// The semantics traversal order.
///
/// This is used to sort this semantic node with all other semantic
/// nodes to determine the traversal order of accessible nodes.
///
/// See also:
///
/// * [SemanticsSortOrder], which manages a list of sort keys.
SemanticsSortOrder get sortOrder => _sortOrder;
SemanticsSortOrder _sortOrder;
set sortOrder(SemanticsSortOrder value) {
assert(value != null);
_sortOrder = value;
_hasBeenAnnotated = true;
}
/// Whether the semantic information provided by the owning [RenderObject] and
/// all of its descendants should be treated as one logical entity.
///
......@@ -2040,6 +2173,7 @@ class SemanticsConfiguration {
_textSelection ??= other._textSelection;
textDirection ??= other.textDirection;
_sortOrder = _sortOrder?.merge(other._sortOrder);
_label = _concatStrings(
thisString: _label,
thisTextDirection: textDirection,
......@@ -2071,6 +2205,7 @@ class SemanticsConfiguration {
.._hasBeenAnnotated = _hasBeenAnnotated
.._isMergingSemanticsOfDescendants = _isMergingSemanticsOfDescendants
.._textDirection = _textDirection
.._sortOrder = _sortOrder
.._label = _label
.._increasedValue = _increasedValue
.._value = _value
......@@ -2094,11 +2229,17 @@ enum DebugSemanticsDumpOrder {
/// the second last, etc. until a taker is found.
inverseHitTest,
/// Print nodes in traversal order.
/// Print nodes in geometric traversal order.
///
/// Traversal order defines how the user can move the accessibility focus from
/// one node to another.
traversal,
/// Geometric traversal order is the default traversal order for semantics nodes which
/// don't have [SemanticsNode.sortOrder] set. This traversal order ignores the node
/// sort order, since the diagnostics system follows the widget tree and can only sort
/// a node's children, and the semantics system sorts nodes globally.
geometricOrder,
// TODO(gspencer): Add support to toStringDeep (and others) to print the tree in
// the actual traversal order that the user will experience. This requires sorting
// nodes globally before printing, not just the children.
}
String _concatStrings({
......@@ -2124,3 +2265,203 @@ String _concatStrings({
return nestedLabel;
return '$thisString\n$nestedLabel';
}
/// Provides a way to specify the order in which semantic nodes are sorted.
///
/// [TranversalSortOrder] objects contain a list of sort keys in the order in
/// which they are applied. They are attached to [Semantics] widgets in the
/// widget hierarchy, and are merged with the sort orders of their parent
/// [Semantics] widgets. If [SemanticsSortOrder.discardParentOrder] is set to
/// true, then they will instead ignore the sort order from the parents.
///
/// Keys at the same position in the sort order are compared with each other,
/// and keys which are of different types, or which have different
/// [SemanticSortKey.name] values compare as "equal" so that two different types
/// of keys can co-exist at the same level and not interfere with each other,
/// allowing for sorting into groups. Keys that evaluate as equal, or when
/// compared with Widgets that don't have [Semantics], fall back to the default
/// upper-start-to-lower-end geometric ordering.
///
/// Since widgets are globally sorted by their sort key, the order does not have
/// to conform to the widget hierarchy.
///
/// This class takes either `key` or `keys` at construction, but not both. The
/// `key` argument is just shorthand for specifying `<SemanticsSortKey>[key]`
/// for the `keys` argument.
///
/// ## Sample code
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return new Column(
/// children: <Widget>[
/// new Semantics(
/// sortOrder: new SemanticsSortOrder(key: new OrdinalSortKey(2.0)),
/// child: const Text('Label One'),
/// ),
/// new Semantics(
/// sortOrder: new SemanticsSortOrder(key: new OrdinalSortKey(2.0)),
/// child: const Text('Label Two'),
/// ),
/// ],
/// );
/// }
/// }
/// ```
///
/// The above will create two [Text] widgets with "Label One" and "Label Two" as
/// their text, but, in accessibility mode, "Label Two" will be traversed first,
/// and "Label One" will be next. Without the sort keys, they would be traversed
/// top to bottom instead.
///
/// See also:
///
/// * [Semantics] for an object that annotates widgets with accessibility
/// semantics.
/// * [SemanticsSortKey] for the base class of the sort keys which
/// [SemanticsSortOrder] manages.
/// * [OrdinalSortKey] for a sort key that sorts using an ordinal.
class SemanticsSortOrder extends Diagnosticable implements Comparable<SemanticsSortOrder> {
/// Only one of `key` or `keys` may be specified, but at least one must
/// be specified. Specifying `key` is a shorthand for specifying
/// `keys = <SemanticsSortKey>[key]`.
///
/// If [discardParentOrder] is set to true, then the
/// [SemanticsSortOrder.keys] will replace the list of keys from the parents
/// when merged, instead of extending them.
SemanticsSortOrder({
SemanticsSortKey key,
List<SemanticsSortKey> keys,
this.discardParentOrder = false,
})
: assert(key != null || keys != null, 'One of key or keys must be specified.'),
assert(key == null || keys == null, 'Only one of key or keys may be specified.'),
keys = key == null ? keys : <SemanticsSortKey>[key];
/// Whether or not this order is to replace the keys above it in the
/// semantics tree, or to be appended to them.
final bool discardParentOrder;
final List<SemanticsSortKey> keys;
/// Merges two sort orders by concatenating their sort key lists. If
/// other.discardParentOrder is true, then other's sort key list replaces
/// that of the list in this object.
SemanticsSortOrder merge(SemanticsSortOrder other) {
if (other == null)
return this;
if (other.discardParentOrder) {
return new SemanticsSortOrder(
keys: new List<SemanticsSortKey>.from(other.keys),
discardParentOrder: discardParentOrder,
);
}
return new SemanticsSortOrder(
keys: new List<SemanticsSortKey>.from(keys)
..addAll(other.keys),
discardParentOrder: discardParentOrder,
);
}
@override
int compareTo(SemanticsSortOrder other) {
if (this == other) {
return 0;
}
for (int i = 0; i < keys.length && i < other.keys.length; ++i) {
final int comparison = keys[i].compareTo(other.keys[i]);
if (comparison != 0) {
return comparison;
}
}
// If there are more keys to compare, then assume that the shorter
// list comes before the longer list.
return keys.length.compareTo(other.keys.length);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new IterableProperty<SemanticsSortKey>('keys', keys, ifEmpty: null));
description.add(new FlagProperty(
'replace',
value: discardParentOrder,
defaultValue: false,
ifTrue: 'replace',
));
}
}
/// Base class for all sort keys for [Semantics] accessibility traversal order
/// sorting.
///
/// If subclasses of this class compare themselves to another subclass of
/// [SemanticsSortKey], they will compare as "equal" so that keys of the same
/// type are ordered only with respect to one another.
///
/// See Also:
///
/// * [SemanticsSortOrder] which manages a list of sort keys.
/// * [OrdinalSortKey] for a sort key that sorts using an ordinal.
abstract class SemanticsSortKey extends Diagnosticable implements Comparable<SemanticsSortKey> {
const SemanticsSortKey({this.name});
/// An optional name that will make this sort key only order itself
/// with respect to other sort keys of the same [name], as long as
/// they are of the same [runtimeType]. If compared with a
/// [SemanticsSortKey] with a different name or type, they will
/// compare as "equal".
final String name;
@override
int compareTo(SemanticsSortKey other) {
if (other.runtimeType != runtimeType || other.name != name) {
return 0;
}
return doCompare(other);
}
@protected
int doCompare(SemanticsSortKey other);
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new StringProperty('name', name, defaultValue: null));
}
}
/// A [SemanticsSortKey] that sorts simply based on the ordinal given it.
///
/// The [OrdinalSortKey] compares itself with other [OrdinalSortKey]s
/// to sort based on the order it is given.
///
/// See also:
///
/// * [SemanticsSortOrder] which manages a list of sort keys.
class OrdinalSortKey extends SemanticsSortKey {
const OrdinalSortKey(this.order, {String name}) : super(name: name);
/// [order] is a double which describes the order in which this node
/// is traversed by the platform's accessibility services. Lower values
/// will be traversed first.
final double order;
@override
int doCompare(SemanticsSortKey other) {
assert(other.runtimeType == runtimeType);
final OrdinalSortKey otherOrder = other;
if (otherOrder.order == null || order == null || otherOrder.order == order) {
return 0;
}
return order.compareTo(otherOrder.order);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder description) {
super.debugFillProperties(description);
description.add(new DoubleProperty('order', order, defaultValue: null));
}
}
......@@ -4828,7 +4828,16 @@ class Semantics extends SingleChildRenderObjectWidget {
/// Creates a semantic annotation.
///
/// The [container] argument must not be null. To create a `const` instance
/// of [Semantics], use the [new Semantics.fromProperties] constructor.
/// of [Semantics], use the [Semantics.fromProperties] constructor.
///
/// Only one of [sortKey] or [sortOrder] may be specified. Specifying [sortKey]
/// is just a shorthand for specifying `new SemanticsSortOrder(key: sortKey)`
/// for the [sortOrder].
///
/// See also:
///
/// * [SemanticsSortOrder] for a class that determines accessibility traversal
/// order.
Semantics({
Key key,
Widget child,
......@@ -4844,6 +4853,8 @@ class Semantics extends SingleChildRenderObjectWidget {
String decreasedValue,
String hint,
TextDirection textDirection,
SemanticsSortOrder sortOrder,
SemanticsSortKey sortKey,
VoidCallback onTap,
VoidCallback onLongPress,
VoidCallback onScrollLeft,
......@@ -4874,6 +4885,7 @@ class Semantics extends SingleChildRenderObjectWidget {
decreasedValue: decreasedValue,
hint: hint,
textDirection: textDirection,
sortOrder: _effectiveSortOrder(sortKey, sortOrder),
onTap: onTap,
onLongPress: onLongPress,
onScrollLeft: onScrollLeft,
......@@ -4887,8 +4899,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onPaste: onPaste,
onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onSetSelection: onSetSelection,
),
onSetSelection: onSetSelection,),
);
/// Creates a semantic annotation using [SemanticsProperties].
......@@ -4904,6 +4915,11 @@ class Semantics extends SingleChildRenderObjectWidget {
assert(properties != null),
super(key: key, child: child);
static SemanticsSortOrder _effectiveSortOrder(SemanticsSortKey sortKey, SemanticsSortOrder sortOrder) {
assert(sortOrder == null || sortKey == null, 'Only one of sortOrder or sortKey may be specified.');
return sortOrder ?? (sortKey != null ? new SemanticsSortOrder(key: sortKey) : null);
}
/// Contains properties used by assistive technologies to make the application
/// more accessible.
final SemanticsProperties properties;
......@@ -4927,8 +4943,8 @@ class Semantics extends SingleChildRenderObjectWidget {
/// information to the semantic tree is to introduce new explicit
/// [SemanticNode]s to the tree.
///
/// This setting is often used in combination with [isSemanticBoundary] to
/// create semantic boundaries that are either writable or not for children.
/// This setting is often used in combination with [SemanticsConfiguration.isSemanticBoundary]
/// to create semantic boundaries that are either writable or not for children.
final bool explicitChildNodes;
@override
......@@ -4946,6 +4962,7 @@ class Semantics extends SingleChildRenderObjectWidget {
decreasedValue: properties.decreasedValue,
hint: properties.hint,
textDirection: _getTextDirection(context),
sortOrder: properties.sortOrder,
onTap: properties.onTap,
onLongPress: properties.onLongPress,
onScrollLeft: properties.onScrollLeft,
......@@ -4989,6 +5006,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..decreasedValue = properties.decreasedValue
..hint = properties.hint
..textDirection = _getTextDirection(context)
..sortOrder = properties.sortOrder
..onTap = properties.onTap
..onLongPress = properties.onLongPress
..onScrollLeft = properties.onScrollLeft
......
......@@ -193,11 +193,11 @@ void main() {
console.clear();
});
test('Service extensions - debugDumpSemanticsTreeInTraversalOrder', () async {
test('Service extensions - debugDumpSemanticsTreeInGeometricOrder', () async {
Map<String, String> result;
await binding.doFrame();
result = await binding.testExtension('debugDumpSemanticsTreeInTraversalOrder', <String, String>{});
result = await binding.testExtension('debugDumpSemanticsTreeInGeometricOrder', <String, String>{});
expect(result, <String, String>{});
expect(console, <String>['Semantics not collected.']);
console.clear();
......
......@@ -4,8 +4,8 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/semantics.dart';
import 'package:test/test.dart';
import 'package:vector_math/vector_math_64.dart';
import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart';
......@@ -125,13 +125,155 @@ void main() {
expect(child2.transform, isNull);
expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.geometricOrder),
'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'├SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
'└SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(5.0, 0.0, 10.0, 5.0))\n',
);
});
test('OrdinalSortKey compares correctly', () {
final List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
<SemanticsSortKey>[const OrdinalSortKey(0.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const OrdinalSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(1.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const OrdinalSortKey(1.0), const OrdinalSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const CustomSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const CustomSortKey(0.0)],
<SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(0.0)],
];
final List<int> expectedResults = <int>[0, -1, 1, 0, 0, 0, 0, 0];
assert(tests.length == expectedResults.length);
final List<int> results = <int>[];
for (List<SemanticsSortKey> tuple in tests) {
results.add(tuple[0].compareTo(tuple[1]));
}
expect(results, orderedEquals(expectedResults));
});
test('SemanticsSortOrder sorts correctly', () {
final SemanticsSortOrder order1 = new SemanticsSortOrder(key: const CustomSortKey(0.0));
final SemanticsSortOrder order2 = new SemanticsSortOrder(key: const CustomSortKey(0.0));
// Equal single keys compare equal.
expect(order1.compareTo(order2), equals(0));
// Key lists that are longer compare as after the shorter ones.
order1.keys.add(const OrdinalSortKey(1.0));
expect(order1.compareTo(order2), equals(1));
// Equal multiple key lists compare equal.
order2.keys.add(const OrdinalSortKey(1.0));
expect(order1.compareTo(order2), equals(0));
// Different types compare equal.
order1.keys.add(const OrdinalSortKey(1.0));
order2.keys.add(const CustomSortKey(1.0));
expect(order1.compareTo(order2), equals(0));
// Unequal multiple-key lists sort the shorter list first.
order1.keys.add(const CustomSortKey(2.0));
expect(order1.compareTo(order2), equals(1));
});
test('SemanticsSortOrder sorts correctly when assigned names', () {
final SemanticsSortOrder order1g1 = new SemanticsSortOrder(key: const CustomSortKey(0.0, name: 'group 1'));
final SemanticsSortOrder order2g1 = new SemanticsSortOrder(key: const CustomSortKey(1.0, name: 'group 1'));
final SemanticsSortOrder order2g2 = new SemanticsSortOrder(key: const CustomSortKey(1.0, name: 'group 2'));
final SemanticsSortOrder order3g2 = new SemanticsSortOrder(key: const OrdinalSortKey(1.0, name: 'group 1'));
// Keys in the same group compare.
expect(order1g1.compareTo(order2g1), equals(-1));
// Keys with different names compare equal.
expect(order1g1.compareTo(order2g2), equals(0));
// Keys with same names but different types compare equal.
expect(order1g1.compareTo(order3g2), equals(0));
});
test('SemanticsSortOrder replaces correctly in merge', () {
final SemanticsSortOrder order1 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)]);
final SemanticsSortOrder order2 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)]);
final SemanticsSortOrder order3 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(1.0)], discardParentOrder: true);
// Equal single keys compare equal.
expect(order1.compareTo(order2), equals(0));
// Merged orders with one that replaces merge correctly.
final SemanticsSortOrder merged = order1.merge(order3);
expect(merged.keys.length, 2);
expect(merged.keys, orderedEquals(order3.keys));
expect(merged.compareTo(order2), 1);
// Merged orders with one that doesn't replace merge correctly.
final SemanticsSortOrder merged2 = order1.merge(order2);
expect(merged2.keys.length, 4);
expect(merged2.keys, orderedEquals(<SemanticsSortKey>[]..addAll(order1.keys)..addAll(order2.keys)));
expect(merged2.compareTo(order2), 1); // (merged2 is longer, so greater than).
});
test('OrdinalSortKey compares correctly', () {
final List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
<SemanticsSortKey>[const OrdinalSortKey(0.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const OrdinalSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(1.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const OrdinalSortKey(1.0), const OrdinalSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const CustomSortKey(1.0)],
<SemanticsSortKey>[const OrdinalSortKey(0.0), const CustomSortKey(0.0)],
<SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)],
<SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(0.0)],
];
final List<int> expectedResults = <int>[0, -1, 1, 0, 0, 0, 0, 0];
assert(tests.length == expectedResults.length);
final List<int> results = <int>[];
for (List<SemanticsSortKey> tuple in tests) {
results.add(tuple[0].compareTo(tuple[1]));
}
expect(results, orderedEquals(expectedResults));
});
test('SemanticsSortOrder sorts correctly', () {
final SemanticsSortOrder order1 = new SemanticsSortOrder(key: const CustomSortKey(0.0));
final SemanticsSortOrder order2 = new SemanticsSortOrder(key: const CustomSortKey(0.0));
// Equal single keys compare equal.
expect(order1.compareTo(order2), equals(0));
// Key lists that are longer compare as after the shorter ones.
order1.keys.add(const OrdinalSortKey(1.0));
expect(order1.compareTo(order2), equals(1));
// Equal multiple key lists compare equal.
order2.keys.add(const OrdinalSortKey(1.0));
expect(order1.compareTo(order2), equals(0));
// Different types compare equal.
order1.keys.add(const OrdinalSortKey(1.0));
order2.keys.add(const CustomSortKey(1.0));
expect(order1.compareTo(order2), equals(0));
// Unequal multiple-key lists sort the shorter list first.
order1.keys.add(const CustomSortKey(2.0));
expect(order1.compareTo(order2), equals(1));
});
test('SemanticsSortOrder sorts correctly when assigned names', () {
final SemanticsSortOrder order1g1 = new SemanticsSortOrder(key: const CustomSortKey(0.0, name: 'group 1'));
final SemanticsSortOrder order2g1 = new SemanticsSortOrder(key: const CustomSortKey(1.0, name: 'group 1'));
final SemanticsSortOrder order2g2 = new SemanticsSortOrder(key: const CustomSortKey(1.0, name: 'group 2'));
final SemanticsSortOrder order3g2 = new SemanticsSortOrder(key: const OrdinalSortKey(1.0, name: 'group 1'));
// Keys in the same group compare.
expect(order1g1.compareTo(order2g1), equals(-1));
// Keys with different names compare equal.
expect(order1g1.compareTo(order2g2), equals(0));
// Keys with same names but different types compare equal.
expect(order1g1.compareTo(order3g2), equals(0));
});
test('SemanticsSortOrder replaces correctly in merge', () {
final SemanticsSortOrder order1 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)]);
final SemanticsSortOrder order2 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(0.0), const OrdinalSortKey(0.0)]);
final SemanticsSortOrder order3 = new SemanticsSortOrder(keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(1.0)], discardParentOrder: true);
// Equal single keys compare equal.
expect(order1.compareTo(order2), equals(0));
// Merged orders with one that replaces merge correctly.
final SemanticsSortOrder merged = order1.merge(order3);
expect(merged.keys.length, 2);
expect(merged.keys, orderedEquals(order3.keys));
expect(merged.compareTo(order2), 1);
// Merged orders with one that doesn't replace merge correctly.
final SemanticsSortOrder merged2 = order1.merge(order2);
expect(merged2.keys.length, 4);
expect(merged2.keys, orderedEquals(<SemanticsSortKey>[]..addAll(order1.keys)..addAll(order2.keys)));
expect(merged2.compareTo(order2), 1); // (merged2 is longer, so greater than).
});
test('toStringDeep respects childOrder parameter', () {
final SemanticsNode child1 = new SemanticsNode()
..rect = new Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
......@@ -144,7 +286,7 @@ void main() {
childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
);
expect(
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
root.toStringDeep(childOrder: DebugSemanticsDumpOrder.geometricOrder),
'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 20.0, 5.0))\n'
'├SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(10.0, 0.0, 15.0, 5.0))\n'
'└SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(15.0, 0.0, 20.0, 5.0))\n',
......@@ -177,7 +319,7 @@ void main() {
);
expect(
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversal),
rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.geometricOrder),
'SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 25.0, 5.0))\n'
'├SemanticsNode#4(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 10.0, 5.0))\n'
'│├SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 5.0, 5.0))\n'
......@@ -201,12 +343,12 @@ void main() {
final SemanticsNode minimalProperties = new SemanticsNode();
expect(
minimalProperties.toStringDeep(),
'SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0))\n',
'SemanticsNode#1(Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible)\n',
);
expect(
minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null)\n'
'SemanticsNode#1(owner: null, isMergedIntoParent: false, mergeAllDescendantsIntoThisNode: false, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), actions: [], isInMutuallyExcusiveGroup: false, isSelected: false, isFocused: false, isButton: false, isTextField: false, invisible, label: "", value: "", increasedValue: "", decreasedValue: "", hint: "", textDirection: null, nextNodeId: null, sortOrder: null)\n'
);
final SemanticsConfiguration config = new SemanticsConfiguration()
......@@ -219,14 +361,15 @@ void main() {
..isSelected = true
..isButton = true
..label = 'Use all the properties'
..textDirection = TextDirection.rtl;
..textDirection = TextDirection.rtl
..sortOrder = new SemanticsSortOrder(keys: <SemanticsSortKey>[const OrdinalSortKey(1.0)]);
final SemanticsNode allProperties = new SemanticsNode()
..rect = new Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
..transform = new Matrix4.translation(new Vector3(10.0, 10.0, 0.0))
..updateWith(config: config, childrenInInversePaintOrder: null);
expect(
allProperties.toStringDeep(),
'SemanticsNode#2(STALE, owner: null, merge boundary ⛔️, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl)\n',
equalsIgnoringHashCodes('SemanticsNode#2(STALE, owner: null, merge boundary ⛔️, Rect.fromLTRB(60.0, 20.0, 80.0, 50.0), actions: [longPress, scrollUp, showOnScreen], unchecked, selected, button, label: "Use all the properties", textDirection: rtl, sortOrder: SemanticsSortOrder#8e690(keys: [OrdinalSortKey#ca2b8(order: 1.0)]))\n'),
);
expect(
allProperties.getSemanticsData().toString(),
......@@ -370,3 +513,7 @@ class TestRender extends RenderProxyBox {
config.onScrollDown = () { };
}
}
class CustomSortKey extends OrdinalSortKey {
const CustomSortKey(double order, {String name}) : super(order, name: name);
}
\ No newline at end of file
......@@ -410,7 +410,8 @@ void main() {
new TestSemantics.rootChild(
id: expectedId,
rect: TestSemantics.fullScreen,
actions: allActions.fold(0, (int previous, SemanticsAction action) => previous | action.index)
actions: allActions.fold(0, (int previous, SemanticsAction action) => previous | action.index),
nextNodeId: -1,
),
],
);
......@@ -569,4 +570,269 @@ void main() {
semantics.dispose();
});
testWidgets('Semantics widgets built in a widget tree are sorted properly', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
int semanticsUpdateCount = 0;
tester.binding.pipelineOwner.ensureSemantics(
listener: () {
semanticsUpdateCount += 1;
}
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
sortKey: const CustomSortKey(0.0),
explicitChildNodes: true,
child: new Column(
children: <Widget>[
new Semantics(sortKey: const CustomSortKey(3.0), child: const Text('Label 1')),
new Semantics(sortKey: const CustomSortKey(2.0), child: const Text('Label 2')),
new Semantics(
sortKey: const CustomSortKey(1.0),
explicitChildNodes: true,
child: new Row(
children: <Widget>[
new Semantics(sortKey: const OrdinalSortKey(3.0), child: const Text('Label 3')),
new Semantics(sortKey: const OrdinalSortKey(2.0), child: const Text('Label 4')),
new Semantics(sortKey: const OrdinalSortKey(1.0), child: const Text('Label 5')),
],
),
),
],
),
),
),
);
expect(semanticsUpdateCount, 1);
expect(semantics, hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
nextNodeId: 5,
children: <TestSemantics>[
new TestSemantics(
label: r'Label 1',
textDirection: TextDirection.ltr,
nextNodeId: -1,
),
new TestSemantics(
label: r'Label 2',
textDirection: TextDirection.ltr,
nextNodeId: 3,
),
new TestSemantics(
nextNodeId: 8,
children: <TestSemantics>[
new TestSemantics(
label: r'Label 3',
textDirection: TextDirection.ltr,
nextNodeId: 4,
),
new TestSemantics(
label: r'Label 4',
textDirection: TextDirection.ltr,
nextNodeId: 6,
),
new TestSemantics(
label: r'Label 5',
textDirection: TextDirection.ltr,
nextNodeId: 7,
),
],
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Semantics widgets built with explicit sort orders are sorted properly', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
int semanticsUpdateCount = 0;
tester.binding.pipelineOwner.ensureSemantics(
listener: () {
semanticsUpdateCount += 1;
}
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Column(
children: <Widget>[
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(3.0), const OrdinalSortKey(5.0)],
),
child: const Text('Label 1'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(2.0), const OrdinalSortKey(4.0)],
),
child: const Text('Label 2'),
),
new Row(
children: <Widget>[
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(3.0)],
),
child: const Text('Label 3'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(2.0)],
),
child: const Text('Label 4'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(1.0)],
),
child: const Text('Label 5'),
),
],
),
],
),
),
);
expect(semanticsUpdateCount, 1);
expect(semantics, hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
label: r'Label 1',
textDirection: TextDirection.ltr,
nextNodeId: -1,
),
new TestSemantics(
label: r'Label 2',
textDirection: TextDirection.ltr,
nextNodeId: 2,
),
new TestSemantics(
label: r'Label 3',
textDirection: TextDirection.ltr,
nextNodeId: 3,
),
new TestSemantics(
label: r'Label 4',
textDirection: TextDirection.ltr,
nextNodeId: 4,
),
new TestSemantics(
label: r'Label 5',
textDirection: TextDirection.ltr,
nextNodeId: 5,
),
],
)
, ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
testWidgets('Semantics widgets built with some discarded sort orders are sorted properly', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
int semanticsUpdateCount = 0;
tester.binding.pipelineOwner.ensureSemantics(
listener: () {
semanticsUpdateCount += 1;
}
);
await tester.pumpWidget(
new Directionality(
textDirection: TextDirection.ltr,
child: new Semantics(
sortKey: const OrdinalSortKey(0.0),
explicitChildNodes: true,
child: new Column(
children: <Widget>[
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(3.0), const OrdinalSortKey(5.0)],
discardParentOrder: true, // Replace this one.
),
child: const Text('Label 1'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(2.0), const OrdinalSortKey(4.0)],
),
child: const Text('Label 2'),
),
new Row(
children: <Widget>[
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(3.0)],
discardParentOrder: true, // Replace this one.
),
child: const Text('Label 3'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(2.0)],
),
child: const Text('Label 4'),
),
new Semantics(
sortOrder: new SemanticsSortOrder(
keys: <SemanticsSortKey>[const CustomSortKey(1.0), const OrdinalSortKey(1.0)],
),
child: const Text('Label 5'),
),
],
),
],
),
),
),
);
expect(semanticsUpdateCount, 1);
expect(semantics, hasSemantics(
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
nextNodeId: 5,
children: <TestSemantics>[
new TestSemantics(
label: r'Label 1',
textDirection: TextDirection.ltr,
nextNodeId: 7,
),
new TestSemantics(
label: r'Label 2',
textDirection: TextDirection.ltr,
nextNodeId: -1,
),
new TestSemantics(
label: r'Label 3',
textDirection: TextDirection.ltr,
nextNodeId: 3,
),
new TestSemantics(
label: r'Label 4',
textDirection: TextDirection.ltr,
nextNodeId: 4,
),
new TestSemantics(
label: r'Label 5',
textDirection: TextDirection.ltr,
nextNodeId: 6,
),
],
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
}
class CustomSortKey extends OrdinalSortKey {
const CustomSortKey(double order, {String name}) : super(order, name: name);
}
......@@ -42,6 +42,7 @@ class TestSemantics {
this.decreasedValue: '',
this.hint: '',
this.textDirection,
this.nextNodeId,
this.rect,
this.transform,
this.textSelection,
......@@ -68,6 +69,7 @@ class TestSemantics {
this.decreasedValue: '',
this.hint: '',
this.textDirection,
this.nextNodeId,
this.transform,
this.textSelection,
this.children: const <TestSemantics>[],
......@@ -103,6 +105,7 @@ class TestSemantics {
this.increasedValue: '',
this.decreasedValue: '',
this.textDirection,
this.nextNodeId,
this.rect,
Matrix4 transform,
this.textSelection,
......@@ -169,6 +172,10 @@ class TestSemantics {
/// is also set.
final TextDirection textDirection;
/// The ID of the node that is next in the semantics traversal order after
/// this node.
final int nextNodeId;
/// The bounding box for this node in its coordinate system.
///
/// Convenient values are available:
......@@ -250,6 +257,8 @@ class TestSemantics {
return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
if (textDirection != null && textDirection != nodeData.textDirection)
return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
if (nextNodeId != null && nextNodeId != nodeData.nextNodeId)
return fail('expected node id $id to have nextNodeId "$nextNodeId" but found "${nodeData.nextNodeId}".');
if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '' || node.increasedValue != '' || node.decreasedValue != '') && nodeData.textDirection == null)
return fail('expected node id $id, which has a label, value, or hint, to have a textDirection, but it did not.');
if (!ignoreRect && rect != nodeData.rect)
......@@ -301,6 +310,8 @@ class TestSemantics {
buf.writeln('$indent hint: \'$hint\',');
if (textDirection != null)
buf.writeln('$indent textDirection: $textDirection,');
if (nextNodeId != null)
buf.writeln('$indent nextNodeId: $nextNodeId,');
if (textSelection?.isValid == true)
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (rect != null)
......@@ -483,8 +494,14 @@ class SemanticsTester {
buf.writeln(' flags: ${_flagsToSemanticsFlagExpression(nodeData.flags)},');
if (nodeData.actions != 0)
buf.writeln(' actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},');
if (node.label != null && node.label.isNotEmpty)
buf.writeln(' label: r\'${node.label}\',');
if (node.label != null && node.label.isNotEmpty) {
final String escapedLabel = node.label.replaceAll('\n', r'\n');
if (escapedLabel == node.label) {
buf.writeln(' label: r\'$escapedLabel\',');
} else {
buf.writeln(' label: \'$escapedLabel\',');
}
}
if (node.value != null && node.value.isNotEmpty)
buf.writeln(' value: r\'${node.value}\',');
if (node.increasedValue != null && node.increasedValue.isNotEmpty)
......@@ -495,6 +512,8 @@ class SemanticsTester {
buf.writeln(' hint: r\'${node.hint}\',');
if (node.textDirection != null)
buf.writeln(' textDirection: ${node.textDirection},');
if (node.nextNodeId != null)
buf.writeln(' nextNodeId: ${node.nextNodeId},');
if (node.hasChildren) {
buf.writeln(' children: <TestSemantics>[');
......
......@@ -105,12 +105,15 @@ void _tests() {
new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
nextNodeId: 4,
children: <TestSemantics>[
new TestSemantics(
nextNodeId: 2,
children: <TestSemantics>[
new TestSemantics(
label: r'Plain text',
textDirection: TextDirection.ltr,
nextNodeId: 3,
),
new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected],
......@@ -121,6 +124,7 @@ void _tests() {
decreasedValue: r'test-decreasedValue',
hint: r'test-hint',
textDirection: TextDirection.rtl,
nextNodeId: -1,
),
],
),
......
......@@ -184,9 +184,9 @@ class FlutterDevice {
await view.uiIsolate.flutterDebugDumpLayerTree();
}
Future<Null> debugDumpSemanticsTreeInTraversalOrder() async {
Future<Null> debugDumpSemanticsTreeInGeometricOrder() async {
for (FlutterView view in views)
await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder();
await view.uiIsolate.flutterDebugDumpSemanticsTreeInGeometricOrder();
}
Future<Null> debugDumpSemanticsTreeInInverseHitTestOrder() async {
......@@ -499,10 +499,10 @@ abstract class ResidentRunner {
await device.debugDumpLayerTree();
}
Future<Null> _debugDumpSemanticsTreeInTraversalOrder() async {
Future<Null> _debugDumpSemanticsTreeInGeometricOrder() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.debugDumpSemanticsTreeInTraversalOrder();
await device.debugDumpSemanticsTreeInGeometricOrder();
}
Future<Null> _debugDumpSemanticsTreeInInverseHitTestOrder() async {
......@@ -685,7 +685,7 @@ abstract class ResidentRunner {
}
} else if (character == 'S') {
if (supportsServiceProtocol) {
await _debugDumpSemanticsTreeInTraversalOrder();
await _debugDumpSemanticsTreeInGeometricOrder();
return true;
}
} else if (character == 'U') {
......@@ -826,12 +826,12 @@ abstract class ResidentRunner {
printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".');
printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
if (isRunningDebug) {
printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S" (traversal order) or "U" (inverse hit test order).');
printStatus('For layers (debugDumpLayerTree), use "L"; for accessibility (debugDumpSemantics), use "S" (for geometric order) or "U" (for inverse hit test order).');
printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".');
printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
} else {
printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for traversal order) or "U" (for inverse hit test order).');
printStatus('To dump the accessibility tree (debugDumpSemantics), press "S" (for geometric order) or "U" (for inverse hit test order).');
}
printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".');
}
......
......@@ -1146,8 +1146,8 @@ class Isolate extends ServiceObjectOwner {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
}
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout);
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInGeometricOrder() {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInGeometricOrder', timeout: kLongRequestTimeout);
}
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment