semantics_debugger.dart 12.9 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// 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.

5
import 'dart:collection';
Hixie's avatar
Hixie committed
6 7 8
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
9
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
Hixie's avatar
Hixie committed
10 11

import 'basic.dart';
12
import 'binding.dart';
Hixie's avatar
Hixie committed
13 14 15
import 'framework.dart';
import 'gesture_detector.dart';

16
/// A widget that visualizes the semantics for the child.
17 18 19
///
/// This widget is useful for understand how an app presents itself to
/// accessibility technology.
20
class SemanticsDebugger extends StatefulWidget {
21 22 23
  /// Creates a widget that visualizes the semantics for the child.
  ///
  /// The [child] argument must not be null.
Hixie's avatar
Hixie committed
24
  const SemanticsDebugger({ Key key, this.child }) : super(key: key);
25

26
  /// The widget below this widget in the tree.
Hixie's avatar
Hixie committed
27
  final Widget child;
28

29
  @override
Hixie's avatar
Hixie committed
30 31 32 33
  _SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
}

class _SemanticsDebuggerState extends State<SemanticsDebugger> {
34 35
  _SemanticsClient _client;

36
  @override
Hixie's avatar
Hixie committed
37 38
  void initState() {
    super.initState();
39 40 41 42
    // TODO(abarth): We shouldn't reach out to the WidgetsBinding.instance
    // static here because we might not be in a tree that's attached to that
    // binding. Instead, we should find a way to get to the PipelineOwner from
    // the BuildContext.
43
    _client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner)
44
      ..addListener(_update);
Hixie's avatar
Hixie committed
45
  }
46 47

  @override
Hixie's avatar
Hixie committed
48
  void dispose() {
49 50 51
    _client
      ..removeListener(_update)
      ..dispose();
Hixie's avatar
Hixie committed
52 53
    super.dispose();
  }
54

Hixie's avatar
Hixie committed
55 56
  void _update() {
    setState(() {
57
      // the generation of the _SemanticsDebuggerListener has changed
Hixie's avatar
Hixie committed
58 59
    });
  }
60

Hixie's avatar
Hixie committed
61 62 63 64 65 66
  Point _lastPointerDownLocation;
  void _handlePointerDown(PointerDownEvent event) {
    setState(() {
      _lastPointerDownLocation = event.position;
    });
  }
67

Hixie's avatar
Hixie committed
68 69
  void _handleTap() {
    assert(_lastPointerDownLocation != null);
70
    _client._performAction(_lastPointerDownLocation, SemanticAction.tap);
Hixie's avatar
Hixie committed
71 72 73 74 75 76
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
  void _handleLongPress() {
    assert(_lastPointerDownLocation != null);
77
    _client._performAction(_lastPointerDownLocation, SemanticAction.longPress);
Hixie's avatar
Hixie committed
78 79 80 81
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
82
  void _handlePanEnd(DragEndDetails details) {
Hixie's avatar
Hixie committed
83
    assert(_lastPointerDownLocation != null);
84
    _client.handlePanEnd(_lastPointerDownLocation, details.velocity);
Hixie's avatar
Hixie committed
85 86 87 88
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
89 90

  @override
Hixie's avatar
Hixie committed
91 92
  Widget build(BuildContext context) {
    return new CustomPaint(
93
      foregroundPainter: new _SemanticsDebuggerPainter(_client.generation, _client, _lastPointerDownLocation),
Hixie's avatar
Hixie committed
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
      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;
119
  final Set<SemanticAction> actions = new Set<SemanticAction>();
Hixie's avatar
Hixie committed
120 121 122 123 124 125 126
  bool hasCheckedState = false;
  bool isChecked = false;
  String label;
  Matrix4 transform;
  Rect rect;
  List<_SemanticsDebuggerEntry> children;

127
  @override
Hixie's avatar
Hixie committed
128
  String toString() {
129 130 131 132 133 134 135 136
    StringBuffer buffer = new StringBuffer();
    buffer.write('_SemanticsDebuggerEntry($id; $rect; "$label"');
    for (SemanticAction action in actions)
      buffer.write('; $action');
    buffer
      ..write('${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}')
      ..write(')');
    return buffer.toString();
Hixie's avatar
Hixie committed
137
  }
138

Hixie's avatar
Hixie committed
139 140 141 142
  String toStringDeep([ String prefix = '']) {
    if (prefix.length > 20)
      return '$prefix<ABORTED>\n';
    String result = '$prefix$this\n';
143 144
    prefix += '  ';
    for (_SemanticsDebuggerEntry child in children) {
Hixie's avatar
Hixie committed
145 146 147 148 149
      result += '${child.toStringDeep(prefix)}';
    }
    return result;
  }

150 151 152 153 154
  void updateWith(mojom.SemanticsNode node) {
    if (node.flags != null) {
      hasCheckedState = node.flags.hasCheckedState;
      isChecked = node.flags.isChecked;
    }
155 156 157 158 159
    if (node.actions != null) {
      actions.clear();
      for (int encodedAction in node.actions)
        actions.add(SemanticAction.values[encodedAction]);
    }
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
    if (node.strings != null) {
      assert(node.strings.label != null);
      label = node.strings.label;
    } else {
      assert(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;
        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 {
        transform = null;
      }
      rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
    }
    _updateMessage();
  }

Hixie's avatar
Hixie committed
187 188 189
  int findDepth() {
    if (children == null || children.isEmpty)
      return 1;
190 191 192
    return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce((int runningDepth, int nextDepth) {
      return math.max(runningDepth, nextDepth);
    }) + 1;
Hixie's avatar
Hixie committed
193 194 195 196 197
  }

  static const TextStyle textStyles = const TextStyle(
    color: const Color(0xFF000000),
    fontSize: 10.0,
198
    height: 0.8
Hixie's avatar
Hixie committed
199 200
  );

201 202 203 204 205 206 207
  bool get _isScrollable {
    return actions.contains(SemanticAction.scrollLeft)
        || actions.contains(SemanticAction.scrollRight)
        || actions.contains(SemanticAction.scrollUp)
        || actions.contains(SemanticAction.scrollDown);
  }

208 209 210 211 212
  bool get _isAdjustable {
    return actions.contains(SemanticAction.increase)
        || actions.contains(SemanticAction.decrease);
  }

Hixie's avatar
Hixie committed
213
  TextPainter textPainter;
214
  void _updateMessage() {
Hixie's avatar
Hixie committed
215 216 217 218 219 220
    List<String> annotations = <String>[];
    bool wantsTap = false;
    if (hasCheckedState) {
      annotations.add(isChecked ? 'checked' : 'unchecked');
      wantsTap = true;
    }
221
    if (actions.contains(SemanticAction.tap)) {
Hixie's avatar
Hixie committed
222 223 224 225 226 227
      if (!wantsTap)
        annotations.add('button');
    } else {
      if (wantsTap)
        annotations.add('disabled');
    }
228
    if (actions.contains(SemanticAction.longPress))
Hixie's avatar
Hixie committed
229
      annotations.add('long-pressable');
230
    if (_isScrollable)
Hixie's avatar
Hixie committed
231
      annotations.add('scrollable');
232 233
    if (_isAdjustable)
      annotations.add('adjustable');
Hixie's avatar
Hixie committed
234 235 236 237 238 239 240 241 242 243 244 245 246 247
    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();
248 249
      textPainter
        ..text = new TextSpan(style: textStyles, text: message)
250
        ..textAlign = TextAlign.center
251
        ..layout(maxWidth: rect.width);
Hixie's avatar
Hixie committed
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
    } 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
267
         ..style = PaintingStyle.fill;
Hixie's avatar
Hixie committed
268 269 270 271
        canvas.drawRect(rect, fill);
      } else {
        Paint fill = new Paint()
         ..color = const Color(0xFFFFFFFF)
272
         ..style = PaintingStyle.fill;
Hixie's avatar
Hixie committed
273 274 275 276
        canvas.drawRect(rect, fill);
        Paint line = new Paint()
         ..strokeWidth = rank * 2.0
         ..color = lineColor
277
         ..style = PaintingStyle.stroke;
Hixie's avatar
Hixie committed
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
        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) {
294 295 296
      Matrix4 invertedTransform = new Matrix4.identity();
      double determinant = invertedTransform.copyInverse(transform);
      if (determinant == 0.0)
Hixie's avatar
Hixie committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
        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;
  }
}

314 315 316
class _SemanticsClient extends ChangeNotifier {
  _SemanticsClient(PipelineOwner pipelineOwner) {
    _semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree);
Hixie's avatar
Hixie committed
317 318
  }

319 320 321 322 323 324 325 326
  SemanticsOwner _semanticsOwner;

  @override
  void dispose() {
    _semanticsOwner.removeListener(_updateSemanticsTree);
    _semanticsOwner = null;
    super.dispose();
  }
Hixie's avatar
Hixie committed
327

328 329
  _SemanticsDebuggerEntry get rootNode => _nodes[0];
  final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{};
Hixie's avatar
Hixie committed
330

331
  _SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) {
332 333 334
    final int id = node.id;
    _SemanticsDebuggerEntry entry = _nodes.putIfAbsent(id, () => new _SemanticsDebuggerEntry(id));
    entry.updateWith(node);
Hixie's avatar
Hixie committed
335
    if (node.children != null) {
336 337 338 339
      if (entry.children != null)
        entry.children.clear();
      else
        entry.children = new List<_SemanticsDebuggerEntry>();
340
      for (mojom.SemanticsNode child in node.children)
Hixie's avatar
Hixie committed
341 342 343 344 345
        entry.children.add(_updateNode(child));
    }
    return entry;
  }

346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
  void _removeDetachedNodes() {
    // TODO(abarth): We should be able to keep this table updated without
    // walking the entire tree.
    Set<int> detachedNodes = new Set<int>.from(_nodes.keys);
    Queue<_SemanticsDebuggerEntry> unvisited = new Queue<_SemanticsDebuggerEntry>();
    unvisited.add(rootNode);
    while (unvisited.isNotEmpty) {
      _SemanticsDebuggerEntry node = unvisited.removeFirst();
      detachedNodes.remove(node.id);
      if (node.children != null)
        unvisited.addAll(node.children);
    }
    for (int id in detachedNodes)
      _nodes.remove(id);
  }

Hixie's avatar
Hixie committed
362 363
  int generation = 0;

364
  void _updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
Hixie's avatar
Hixie committed
365
    generation += 1;
366
    for (mojom.SemanticsNode node in nodes)
Hixie's avatar
Hixie committed
367
      _updateNode(node);
368 369
    _removeDetachedNodes();
    notifyListeners();
Hixie's avatar
Hixie committed
370 371 372
  }

  _SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
373
    return rootNode?.hitTest(position, filter);
Hixie's avatar
Hixie committed
374 375
  }

376 377
  void _performAction(Point position, SemanticAction action) {
    _SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action));
378
    _semanticsOwner.performAction(entry?.id ?? 0, action);
Hixie's avatar
Hixie committed
379
  }
380

381 382 383 384
  void handlePanEnd(Point position, Velocity velocity) {
    double vx = velocity.pixelsPerSecond.dx;
    double vy = velocity.pixelsPerSecond.dy;
    if (vx.abs() == vy.abs())
Hixie's avatar
Hixie committed
385
      return;
386
    if (vx.abs() > vy.abs()) {
387 388
      if (vx.sign < 0) {
        _performAction(position, SemanticAction.decrease);
389
        _performAction(position, SemanticAction.scrollLeft);
390 391
      } else {
        _performAction(position, SemanticAction.increase);
392
        _performAction(position, SemanticAction.scrollRight);
393
      }
Hixie's avatar
Hixie committed
394
    } else {
395
      if (vy.sign < 0)
396
        _performAction(position, SemanticAction.scrollUp);
Hixie's avatar
Hixie committed
397
      else
398
        _performAction(position, SemanticAction.scrollDown);
Hixie's avatar
Hixie committed
399 400 401 402 403
    }
  }
}

class _SemanticsDebuggerPainter extends CustomPainter {
404
  const _SemanticsDebuggerPainter(this.generation, this.client, this.pointerPosition);
405

Hixie's avatar
Hixie committed
406
  final int generation;
407
  final _SemanticsClient client;
Hixie's avatar
Hixie committed
408
  final Point pointerPosition;
409 410

  @override
Hixie's avatar
Hixie committed
411
  void paint(Canvas canvas, Size size) {
412
    _SemanticsDebuggerEntry rootNode = client.rootNode;
413
    rootNode?.paint(canvas, rootNode.findDepth());
Hixie's avatar
Hixie committed
414 415 416 417 418 419
    if (pointerPosition != null) {
      Paint paint = new Paint();
      paint.color = const Color(0x7F0090FF);
      canvas.drawCircle(pointerPosition, 10.0, paint);
    }
  }
420 421

  @override
Hixie's avatar
Hixie committed
422 423
  bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
    return generation != oldDelegate.generation
424
        || client != oldDelegate.client
Hixie's avatar
Hixie committed
425 426 427
        || pointerPosition != oldDelegate.pointerPosition;
  }
}