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> { ...@@ -130,7 +130,7 @@ class StockHomeState extends State<StockHome> {
debugDumpApp(); debugDumpApp();
debugDumpRenderTree(); debugDumpRenderTree();
debugDumpLayerTree(); debugDumpLayerTree();
debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversal); debugDumpSemanticsTree(DebugSemanticsDumpOrder.geometricOrder);
} catch (e, stack) { } catch (e, stack) {
debugPrint('Exception while dumping app:\n$e\n$stack'); debugPrint('Exception while dumping app:\n$e\n$stack');
} }
......
...@@ -101,8 +101,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul ...@@ -101,8 +101,8 @@ abstract class RendererBinding extends BindingBase with ServicesBinding, Schedul
); );
registerSignalServiceExtension( registerSignalServiceExtension(
name: 'debugDumpSemanticsTreeInTraversalOrder', name: 'debugDumpSemanticsTreeInGeometricOrder',
callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.traversal); return debugPrintDone; } callback: () { debugDumpSemanticsTree(DebugSemanticsDumpOrder.geometricOrder); return debugPrintDone; }
); );
registerSignalServiceExtension( registerSignalServiceExtension(
......
...@@ -2228,7 +2228,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2228,7 +2228,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// Dirty the semantics tree starting at `this` until we have reached a // Dirty the semantics tree starting at `this` until we have reached a
// RenderObject that is a semantics boundary. All semantics past this // RenderObject that is a semantics boundary. All semantics past this
// RenderObject are still up-to date. Therefore, we will later only rebuild // 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; final bool wasSemanticsBoundary = _semantics != null && _cachedSemanticsConfiguration?.isSemanticBoundary == true;
_cachedSemanticsConfiguration = null; _cachedSemanticsConfiguration = null;
...@@ -2254,7 +2254,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2254,7 +2254,7 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
// remove it as it is no longer guaranteed that its semantics // 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 // 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 // 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. // updated.
// (See semantics_10_test.dart for an example why this is required). // (See semantics_10_test.dart for an example why this is required).
owner._nodesNeedingSemantics.remove(this); owner._nodesNeedingSemantics.remove(this);
...@@ -3005,8 +3005,8 @@ class _ContainerSemanticsFragment extends _SemanticsFragment { ...@@ -3005,8 +3005,8 @@ class _ContainerSemanticsFragment extends _SemanticsFragment {
/// A [_SemanticsFragment] that describes which concrete semantic information /// A [_SemanticsFragment] that describes which concrete semantic information
/// a [RenderObject] wants to add to the [SemanticsNode] of its parent. /// a [RenderObject] wants to add to the [SemanticsNode] of its parent.
/// ///
/// Specifically, it describes what children (as returned by [compileChildren]) /// Specifically, it describes which children (as returned by [compileChildren])
/// should be added to the parent's [SemanticsNode] and what [config] should be /// should be added to the parent's [SemanticsNode] and which [config] should be
/// merged into the parent's [SemanticsNode]. /// merged into the parent's [SemanticsNode].
abstract class _InterestingSemanticsFragment extends _SemanticsFragment { abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
_InterestingSemanticsFragment({ _InterestingSemanticsFragment({
...@@ -3082,7 +3082,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment { ...@@ -3082,7 +3082,7 @@ abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
/// An [_InterestingSemanticsFragment] that produces the root [SemanticsNode] of /// An [_InterestingSemanticsFragment] that produces the root [SemanticsNode] of
/// the semantics tree. /// 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]. /// [children].
class _RootSemanticsFragment extends _InterestingSemanticsFragment { class _RootSemanticsFragment extends _InterestingSemanticsFragment {
_RootSemanticsFragment({ _RootSemanticsFragment({
...@@ -3144,7 +3144,7 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment { ...@@ -3144,7 +3144,7 @@ class _RootSemanticsFragment extends _InterestingSemanticsFragment {
/// fragment it will create a new [SemanticsNode]. The newly created node will /// fragment it will create a new [SemanticsNode]. The newly created node will
/// be annotated with the [SemanticsConfiguration] that - without the call to /// be annotated with the [SemanticsConfiguration] that - without the call to
/// [markAsExplicit] - would have been merged into the parent's [SemanticsNode]. /// [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]. /// would have been added to the parent's [SemanticsNode].
/// ///
/// After a call to [markAsExplicit] the only element returned by [children] /// After a call to [markAsExplicit] the only element returned by [children]
......
...@@ -3008,6 +3008,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3008,6 +3008,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
String decreasedValue, String decreasedValue,
String hint, String hint,
TextDirection textDirection, TextDirection textDirection,
SemanticsSortOrder sortOrder,
VoidCallback onTap, VoidCallback onTap,
VoidCallback onLongPress, VoidCallback onLongPress,
VoidCallback onScrollLeft, VoidCallback onScrollLeft,
...@@ -3035,6 +3036,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3035,6 +3036,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_decreasedValue = decreasedValue, _decreasedValue = decreasedValue,
_hint = hint, _hint = hint,
_textDirection = textDirection, _textDirection = textDirection,
_sortOrder = sortOrder,
_onTap = onTap, _onTap = onTap,
_onLongPress = onLongPress, _onLongPress = onLongPress,
_onScrollLeft = onScrollLeft, _onScrollLeft = onScrollLeft,
...@@ -3208,6 +3210,20 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3208,6 +3210,20 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); 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]. /// The handler for [SemanticsAction.tap].
/// ///
/// This is the semantic equivalent of a user briefly tapping the screen with /// This is the semantic equivalent of a user briefly tapping the screen with
...@@ -3504,6 +3520,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3504,6 +3520,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.hint = hint; config.hint = hint;
if (textDirection != null) if (textDirection != null)
config.textDirection = textDirection; config.textDirection = textDirection;
if (sortOrder != null)
config.sortOrder = sortOrder;
// Registering _perform* as action handlers instead of the user provided // Registering _perform* as action handlers instead of the user provided
// ones to ensure that changing a user provided handler from a non-null to // ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update. // another non-null value doesn't require a semantics update.
......
...@@ -4828,7 +4828,16 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4828,7 +4828,16 @@ class Semantics extends SingleChildRenderObjectWidget {
/// Creates a semantic annotation. /// Creates a semantic annotation.
/// ///
/// The [container] argument must not be null. To create a `const` instance /// 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({ Semantics({
Key key, Key key,
Widget child, Widget child,
...@@ -4844,6 +4853,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4844,6 +4853,8 @@ class Semantics extends SingleChildRenderObjectWidget {
String decreasedValue, String decreasedValue,
String hint, String hint,
TextDirection textDirection, TextDirection textDirection,
SemanticsSortOrder sortOrder,
SemanticsSortKey sortKey,
VoidCallback onTap, VoidCallback onTap,
VoidCallback onLongPress, VoidCallback onLongPress,
VoidCallback onScrollLeft, VoidCallback onScrollLeft,
...@@ -4874,6 +4885,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4874,6 +4885,7 @@ class Semantics extends SingleChildRenderObjectWidget {
decreasedValue: decreasedValue, decreasedValue: decreasedValue,
hint: hint, hint: hint,
textDirection: textDirection, textDirection: textDirection,
sortOrder: _effectiveSortOrder(sortKey, sortOrder),
onTap: onTap, onTap: onTap,
onLongPress: onLongPress, onLongPress: onLongPress,
onScrollLeft: onScrollLeft, onScrollLeft: onScrollLeft,
...@@ -4887,8 +4899,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4887,8 +4899,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onPaste: onPaste, onPaste: onPaste,
onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter, onMoveCursorForwardByCharacter: onMoveCursorForwardByCharacter,
onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter, onMoveCursorBackwardByCharacter: onMoveCursorBackwardByCharacter,
onSetSelection: onSetSelection, onSetSelection: onSetSelection,),
),
); );
/// Creates a semantic annotation using [SemanticsProperties]. /// Creates a semantic annotation using [SemanticsProperties].
...@@ -4904,6 +4915,11 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4904,6 +4915,11 @@ class Semantics extends SingleChildRenderObjectWidget {
assert(properties != null), assert(properties != null),
super(key: key, child: child); 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 /// Contains properties used by assistive technologies to make the application
/// more accessible. /// more accessible.
final SemanticsProperties properties; final SemanticsProperties properties;
...@@ -4927,8 +4943,8 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4927,8 +4943,8 @@ class Semantics extends SingleChildRenderObjectWidget {
/// information to the semantic tree is to introduce new explicit /// information to the semantic tree is to introduce new explicit
/// [SemanticNode]s to the tree. /// [SemanticNode]s to the tree.
/// ///
/// This setting is often used in combination with [isSemanticBoundary] to /// This setting is often used in combination with [SemanticsConfiguration.isSemanticBoundary]
/// create semantic boundaries that are either writable or not for children. /// to create semantic boundaries that are either writable or not for children.
final bool explicitChildNodes; final bool explicitChildNodes;
@override @override
...@@ -4946,6 +4962,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4946,6 +4962,7 @@ class Semantics extends SingleChildRenderObjectWidget {
decreasedValue: properties.decreasedValue, decreasedValue: properties.decreasedValue,
hint: properties.hint, hint: properties.hint,
textDirection: _getTextDirection(context), textDirection: _getTextDirection(context),
sortOrder: properties.sortOrder,
onTap: properties.onTap, onTap: properties.onTap,
onLongPress: properties.onLongPress, onLongPress: properties.onLongPress,
onScrollLeft: properties.onScrollLeft, onScrollLeft: properties.onScrollLeft,
...@@ -4989,6 +5006,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4989,6 +5006,7 @@ class Semantics extends SingleChildRenderObjectWidget {
..decreasedValue = properties.decreasedValue ..decreasedValue = properties.decreasedValue
..hint = properties.hint ..hint = properties.hint
..textDirection = _getTextDirection(context) ..textDirection = _getTextDirection(context)
..sortOrder = properties.sortOrder
..onTap = properties.onTap ..onTap = properties.onTap
..onLongPress = properties.onLongPress ..onLongPress = properties.onLongPress
..onScrollLeft = properties.onScrollLeft ..onScrollLeft = properties.onScrollLeft
......
...@@ -193,11 +193,11 @@ void main() { ...@@ -193,11 +193,11 @@ void main() {
console.clear(); console.clear();
}); });
test('Service extensions - debugDumpSemanticsTreeInTraversalOrder', () async { test('Service extensions - debugDumpSemanticsTreeInGeometricOrder', () async {
Map<String, String> result; Map<String, String> result;
await binding.doFrame(); await binding.doFrame();
result = await binding.testExtension('debugDumpSemanticsTreeInTraversalOrder', <String, String>{}); result = await binding.testExtension('debugDumpSemanticsTreeInGeometricOrder', <String, String>{});
expect(result, <String, String>{}); expect(result, <String, String>{});
expect(console, <String>['Semantics not collected.']); expect(console, <String>['Semantics not collected.']);
console.clear(); console.clear();
......
...@@ -410,7 +410,8 @@ void main() { ...@@ -410,7 +410,8 @@ void main() {
new TestSemantics.rootChild( new TestSemantics.rootChild(
id: expectedId, id: expectedId,
rect: TestSemantics.fullScreen, 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() { ...@@ -569,4 +570,269 @@ void main() {
semantics.dispose(); 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 { ...@@ -42,6 +42,7 @@ class TestSemantics {
this.decreasedValue: '', this.decreasedValue: '',
this.hint: '', this.hint: '',
this.textDirection, this.textDirection,
this.nextNodeId,
this.rect, this.rect,
this.transform, this.transform,
this.textSelection, this.textSelection,
...@@ -68,6 +69,7 @@ class TestSemantics { ...@@ -68,6 +69,7 @@ class TestSemantics {
this.decreasedValue: '', this.decreasedValue: '',
this.hint: '', this.hint: '',
this.textDirection, this.textDirection,
this.nextNodeId,
this.transform, this.transform,
this.textSelection, this.textSelection,
this.children: const <TestSemantics>[], this.children: const <TestSemantics>[],
...@@ -103,6 +105,7 @@ class TestSemantics { ...@@ -103,6 +105,7 @@ class TestSemantics {
this.increasedValue: '', this.increasedValue: '',
this.decreasedValue: '', this.decreasedValue: '',
this.textDirection, this.textDirection,
this.nextNodeId,
this.rect, this.rect,
Matrix4 transform, Matrix4 transform,
this.textSelection, this.textSelection,
...@@ -169,6 +172,10 @@ class TestSemantics { ...@@ -169,6 +172,10 @@ class TestSemantics {
/// is also set. /// is also set.
final TextDirection textDirection; 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. /// The bounding box for this node in its coordinate system.
/// ///
/// Convenient values are available: /// Convenient values are available:
...@@ -250,6 +257,8 @@ class TestSemantics { ...@@ -250,6 +257,8 @@ class TestSemantics {
return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".'); return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
if (textDirection != null && textDirection != nodeData.textDirection) if (textDirection != null && textDirection != nodeData.textDirection)
return fail('expected node id $id to have textDirection "$textDirection" but found "${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) 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.'); 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) if (!ignoreRect && rect != nodeData.rect)
...@@ -301,6 +310,8 @@ class TestSemantics { ...@@ -301,6 +310,8 @@ class TestSemantics {
buf.writeln('$indent hint: \'$hint\','); buf.writeln('$indent hint: \'$hint\',');
if (textDirection != null) if (textDirection != null)
buf.writeln('$indent textDirection: $textDirection,'); buf.writeln('$indent textDirection: $textDirection,');
if (nextNodeId != null)
buf.writeln('$indent nextNodeId: $nextNodeId,');
if (textSelection?.isValid == true) if (textSelection?.isValid == true)
buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],'); buf.writeln('$indent textSelection:\n[${textSelection.start}, ${textSelection.end}],');
if (rect != null) if (rect != null)
...@@ -483,8 +494,14 @@ class SemanticsTester { ...@@ -483,8 +494,14 @@ class SemanticsTester {
buf.writeln(' flags: ${_flagsToSemanticsFlagExpression(nodeData.flags)},'); buf.writeln(' flags: ${_flagsToSemanticsFlagExpression(nodeData.flags)},');
if (nodeData.actions != 0) if (nodeData.actions != 0)
buf.writeln(' actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},'); buf.writeln(' actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},');
if (node.label != null && node.label.isNotEmpty) if (node.label != null && node.label.isNotEmpty) {
buf.writeln(' label: r\'${node.label}\','); 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) if (node.value != null && node.value.isNotEmpty)
buf.writeln(' value: r\'${node.value}\','); buf.writeln(' value: r\'${node.value}\',');
if (node.increasedValue != null && node.increasedValue.isNotEmpty) if (node.increasedValue != null && node.increasedValue.isNotEmpty)
...@@ -495,6 +512,8 @@ class SemanticsTester { ...@@ -495,6 +512,8 @@ class SemanticsTester {
buf.writeln(' hint: r\'${node.hint}\','); buf.writeln(' hint: r\'${node.hint}\',');
if (node.textDirection != null) if (node.textDirection != null)
buf.writeln(' textDirection: ${node.textDirection},'); buf.writeln(' textDirection: ${node.textDirection},');
if (node.nextNodeId != null)
buf.writeln(' nextNodeId: ${node.nextNodeId},');
if (node.hasChildren) { if (node.hasChildren) {
buf.writeln(' children: <TestSemantics>['); buf.writeln(' children: <TestSemantics>[');
......
...@@ -105,12 +105,15 @@ void _tests() { ...@@ -105,12 +105,15 @@ void _tests() {
new TestSemantics( new TestSemantics(
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
nextNodeId: 4,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
nextNodeId: 2,
children: <TestSemantics>[ children: <TestSemantics>[
new TestSemantics( new TestSemantics(
label: r'Plain text', label: r'Plain text',
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
nextNodeId: 3,
), ),
new TestSemantics( new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected], flags: <SemanticsFlag>[SemanticsFlag.hasCheckedState, SemanticsFlag.isChecked, SemanticsFlag.isSelected],
...@@ -121,6 +124,7 @@ void _tests() { ...@@ -121,6 +124,7 @@ void _tests() {
decreasedValue: r'test-decreasedValue', decreasedValue: r'test-decreasedValue',
hint: r'test-hint', hint: r'test-hint',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
nextNodeId: -1,
), ),
], ],
), ),
......
...@@ -184,9 +184,9 @@ class FlutterDevice { ...@@ -184,9 +184,9 @@ class FlutterDevice {
await view.uiIsolate.flutterDebugDumpLayerTree(); await view.uiIsolate.flutterDebugDumpLayerTree();
} }
Future<Null> debugDumpSemanticsTreeInTraversalOrder() async { Future<Null> debugDumpSemanticsTreeInGeometricOrder() async {
for (FlutterView view in views) for (FlutterView view in views)
await view.uiIsolate.flutterDebugDumpSemanticsTreeInTraversalOrder(); await view.uiIsolate.flutterDebugDumpSemanticsTreeInGeometricOrder();
} }
Future<Null> debugDumpSemanticsTreeInInverseHitTestOrder() async { Future<Null> debugDumpSemanticsTreeInInverseHitTestOrder() async {
...@@ -499,10 +499,10 @@ abstract class ResidentRunner { ...@@ -499,10 +499,10 @@ abstract class ResidentRunner {
await device.debugDumpLayerTree(); await device.debugDumpLayerTree();
} }
Future<Null> _debugDumpSemanticsTreeInTraversalOrder() async { Future<Null> _debugDumpSemanticsTreeInGeometricOrder() async {
await refreshViews(); await refreshViews();
for (FlutterDevice device in flutterDevices) for (FlutterDevice device in flutterDevices)
await device.debugDumpSemanticsTreeInTraversalOrder(); await device.debugDumpSemanticsTreeInGeometricOrder();
} }
Future<Null> _debugDumpSemanticsTreeInInverseHitTestOrder() async { Future<Null> _debugDumpSemanticsTreeInInverseHitTestOrder() async {
...@@ -685,7 +685,7 @@ abstract class ResidentRunner { ...@@ -685,7 +685,7 @@ abstract class ResidentRunner {
} }
} else if (character == 'S') { } else if (character == 'S') {
if (supportsServiceProtocol) { if (supportsServiceProtocol) {
await _debugDumpSemanticsTreeInTraversalOrder(); await _debugDumpSemanticsTreeInGeometricOrder();
return true; return true;
} }
} else if (character == 'U') { } else if (character == 'U') {
...@@ -826,12 +826,12 @@ abstract class ResidentRunner { ...@@ -826,12 +826,12 @@ abstract class ResidentRunner {
printStatus('You can dump the widget hierarchy of the app (debugDumpApp) by pressing "w".'); 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".'); printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
if (isRunningDebug) { 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 widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".');
printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".'); printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".'); printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
} else { } 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".'); printStatus('To display the performance overlay (WidgetsApp.showPerformanceOverlay), press "P".');
} }
......
...@@ -1146,8 +1146,8 @@ class Isolate extends ServiceObjectOwner { ...@@ -1146,8 +1146,8 @@ class Isolate extends ServiceObjectOwner {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout); return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpLayerTree', timeout: kLongRequestTimeout);
} }
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInTraversalOrder() { Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInGeometricOrder() {
return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInTraversalOrder', timeout: kLongRequestTimeout); return invokeFlutterExtensionRpcRaw('ext.flutter.debugDumpSemanticsTreeInGeometricOrder', timeout: kLongRequestTimeout);
} }
Future<Map<String, dynamic>> flutterDebugDumpSemanticsTreeInInverseHitTestOrder() { 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