flutter_flutter/packages/flutter/lib/src/widgets/semantics_debugger.dart
Adam Barth 65cb3ee769 Add dartdoc for child widgets
This patch adds a consistent, simple description for the common pattern of
having a single child widget.
2016-03-18 15:19:36 -07:00

388 lines
12 KiB
Dart

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
/// Visualizes the semantics for the child.
///
/// This widget is useful for understand how an app presents itself to
/// accessibility technology.
class SemanticsDebugger extends StatefulWidget {
const SemanticsDebugger({ Key key, this.child }) : super(key: key);
/// The widget below this widget in the tree.
final Widget child;
@override
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
}
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
@override
void initState() {
super.initState();
_SemanticsDebuggerListener.ensureInstantiated();
_SemanticsDebuggerListener.instance.addListener(_update);
}
@override
void dispose() {
_SemanticsDebuggerListener.instance.removeListener(_update);
super.dispose();
}
void _update() {
setState(() {
// the generation of the _SemanticsDebuggerListener has changed
});
}
Point _lastPointerDownLocation;
void _handlePointerDown(PointerDownEvent event) {
setState(() {
_lastPointerDownLocation = event.position;
});
}
void _handleTap() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handleTap(_lastPointerDownLocation);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handleLongPress() {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handleLongPress(_lastPointerDownLocation);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handlePanEnd(Velocity velocity) {
assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, velocity);
setState(() {
_lastPointerDownLocation = null;
});
}
@override
Widget build(BuildContext context) {
return new CustomPaint(
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerListener.instance.generation, _lastPointerDownLocation),
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTap,
onLongPress: _handleLongPress,
onPanEnd: _handlePanEnd,
excludeFromSemantics: true, // otherwise if you don't hit anything, we end up receiving it, which causes an infinite loop...
child: new Listener(
onPointerDown: _handlePointerDown,
behavior: HitTestBehavior.opaque,
child: new IgnorePointer(
ignoringSemantics: false,
child: config.child
)
)
)
);
}
}
typedef bool _SemanticsDebuggerEntryFilter(_SemanticsDebuggerEntry entry);
class _SemanticsDebuggerEntry {
_SemanticsDebuggerEntry(this.id);
final int id;
bool canBeTapped = false;
bool canBeLongPressed = false;
bool canBeScrolledHorizontally = false;
bool canBeScrolledVertically = false;
bool hasCheckedState = false;
bool isChecked = false;
String label;
Matrix4 transform;
Rect rect;
List<_SemanticsDebuggerEntry> children;
@override
String toString() {
return '_SemanticsDebuggerEntry($id; $rect; "$label"'
'${canBeTapped ? "; canBeTapped" : ""}'
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
'${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}'
')';
}
String toStringDeep([ String prefix = '']) {
if (prefix.length > 20)
return '$prefix<ABORTED>\n';
String result = '$prefix$this\n';
for (_SemanticsDebuggerEntry child in children.reversed) {
prefix += ' ';
result += '${child.toStringDeep(prefix)}';
}
return result;
}
int findDepth() {
if (children == null || children.isEmpty)
return 1;
return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce((int runningDepth, int nextDepth) {
return math.max(runningDepth, nextDepth);
}) + 1;
}
static const TextStyle textStyles = const TextStyle(
color: const Color(0xFF000000),
fontSize: 10.0,
height: 0.8,
textAlign: TextAlign.center
);
TextPainter textPainter;
void updateMessage() {
List<String> annotations = <String>[];
bool wantsTap = false;
if (hasCheckedState) {
annotations.add(isChecked ? 'checked' : 'unchecked');
wantsTap = true;
}
if (canBeTapped) {
if (!wantsTap)
annotations.add('button');
} else {
if (wantsTap)
annotations.add('disabled');
}
if (canBeLongPressed)
annotations.add('long-pressable');
if (canBeScrolledHorizontally || canBeScrolledVertically)
annotations.add('scrollable');
String message;
if (annotations.isEmpty) {
assert(label != null);
message = label;
} else {
if (label == '') {
message = annotations.join('; ');
} else {
message = '$label (${annotations.join('; ')})';
}
}
message = message.trim();
if (message != '') {
textPainter ??= new TextPainter();
textPainter.text = new TextSpan(style: textStyles, text: message);
textPainter.maxWidth = rect.width;
textPainter.maxHeight = rect.height;
textPainter.layout();
} else {
textPainter = null;
}
}
void paint(Canvas canvas, int rank) {
canvas.save();
if (transform != null)
canvas.transform(transform.storage);
if (!rect.isEmpty) {
Color lineColor = new Color(0xFF000000 + new math.Random(id).nextInt(0xFFFFFF));
Rect innerRect = rect.deflate(rank * 1.0);
if (innerRect.isEmpty) {
Paint fill = new Paint()
..color = lineColor
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
} else {
Paint fill = new Paint()
..color = const Color(0xFFFFFFFF)
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
Paint line = new Paint()
..strokeWidth = rank * 2.0
..color = lineColor
..style = PaintingStyle.stroke;
canvas.drawRect(innerRect, line);
}
if (textPainter != null) {
canvas.save();
canvas.clipRect(rect);
textPainter.paint(canvas, rect.topLeft.toOffset());
canvas.restore();
}
}
for (_SemanticsDebuggerEntry child in children)
child.paint(canvas, rank - 1);
canvas.restore();
}
_SemanticsDebuggerEntry hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
if (transform != null) {
Matrix4 invertedTransform = new Matrix4.identity();
double determinant = invertedTransform.copyInverse(transform);
if (determinant == 0.0)
return null;
position = MatrixUtils.transformPoint(invertedTransform, position);
}
if (!rect.contains(position))
return null;
_SemanticsDebuggerEntry result;
for (_SemanticsDebuggerEntry child in children.reversed) {
result = child.hitTest(position, filter);
if (result != null)
break;
}
if (result == null || !filter(result))
result = this;
return result;
}
}
class _SemanticsDebuggerListener implements mojom.SemanticsListener {
_SemanticsDebuggerListener._() {
SemanticsNode.addListener(this);
}
static _SemanticsDebuggerListener instance;
static final SemanticsServer _server = new SemanticsServer();
static void ensureInstantiated() {
instance ??= new _SemanticsDebuggerListener._();
}
Set<VoidCallback> _listeners = new Set<VoidCallback>();
void addListener(VoidCallback callback) {
assert(!_listeners.contains(callback));
_listeners.add(callback);
}
void removeListener(VoidCallback callback) {
_listeners.remove(callback);
}
Map<int, _SemanticsDebuggerEntry> nodes = <int, _SemanticsDebuggerEntry>{};
_SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) {
_SemanticsDebuggerEntry entry = nodes.putIfAbsent(node.id, () => new _SemanticsDebuggerEntry(node.id));
if (node.flags != null) {
entry.canBeTapped = node.flags.canBeTapped;
entry.canBeLongPressed = node.flags.canBeLongPressed;
entry.canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
entry.canBeScrolledVertically = node.flags.canBeScrolledVertically;
entry.hasCheckedState = node.flags.hasCheckedState;
entry.isChecked = node.flags.isChecked;
}
if (node.strings != null) {
assert(node.strings.label != null);
entry.label = node.strings.label;
} else {
assert(entry.label != null);
}
if (node.geometry != null) {
if (node.geometry.transform != null) {
assert(node.geometry.transform.length == 16);
// TODO(ianh): Replace this with a cleaner call once
// https://github.com/google/vector_math.dart/issues/159
// is fixed.
List<double> array = node.geometry.transform;
entry.transform = new Matrix4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
} else {
entry.transform = null;
}
entry.rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
}
entry.updateMessage();
if (node.children != null) {
Set<_SemanticsDebuggerEntry> oldChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children ?? const <_SemanticsDebuggerEntry>[]);
entry.children?.clear();
entry.children ??= new List<_SemanticsDebuggerEntry>();
for (mojom.SemanticsNode child in node.children)
entry.children.add(_updateNode(child));
Set<_SemanticsDebuggerEntry> newChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children);
Set<_SemanticsDebuggerEntry> removedChildren = oldChildren.difference(newChildren);
for (_SemanticsDebuggerEntry oldChild in removedChildren)
nodes.remove(oldChild.id);
}
return entry;
}
int generation = 0;
@override
void updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
generation += 1;
for (mojom.SemanticsNode node in nodes)
_updateNode(node);
for (VoidCallback listener in _listeners)
listener();
}
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
return nodes[0]?.hitTest(position, filter);
}
void handleTap(Point position) {
_server.tap(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeTapped)?.id ?? 0);
}
void handleLongPress(Point position) {
_server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0);
}
void handlePanEnd(Point position, Velocity velocity) {
double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.pixelsPerSecond.dy;
if (vx.abs() == vy.abs())
return;
if (vx.abs() > vy.abs()) {
if (vx.sign < 0)
_server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
else
_server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
} else {
if (vy.sign < 0)
_server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
else
_server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
}
}
}
class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition);
final int generation;
final Point pointerPosition;
@override
void paint(Canvas canvas, Size size) {
_SemanticsDebuggerListener.instance.nodes[0]?.paint(
canvas,
_SemanticsDebuggerListener.instance.nodes[0].findDepth()
);
if (pointerPosition != null) {
Paint paint = new Paint();
paint.color = const Color(0x7F0090FF);
canvas.drawCircle(pointerPosition, 10.0, paint);
}
}
@override
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
return generation != oldDelegate.generation
|| pointerPosition != oldDelegate.pointerPosition;
}
}