semantics_debugger.dart 11.2 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4 5
// 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;
6
import 'dart:ui' show SemanticsFlags;
7
import 'dart:ui' as ui show window;
Hixie's avatar
Hixie committed
8

9 10
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
Hixie's avatar
Hixie committed
11 12 13
import 'package:flutter/rendering.dart';

import 'basic.dart';
14
import 'binding.dart';
Hixie's avatar
Hixie committed
15 16 17
import 'framework.dart';
import 'gesture_detector.dart';

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

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

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

35
class _SemanticsDebuggerState extends State<SemanticsDebugger> with WidgetsBindingObserver {
36 37
  _SemanticsClient _client;

38
  @override
Hixie's avatar
Hixie committed
39 40
  void initState() {
    super.initState();
41 42 43 44
    // 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.
45
    _client = new _SemanticsClient(WidgetsBinding.instance.pipelineOwner)
46
      ..addListener(_update);
47
    WidgetsBinding.instance.addObserver(this);
Hixie's avatar
Hixie committed
48
  }
49 50

  @override
Hixie's avatar
Hixie committed
51
  void dispose() {
52 53 54
    _client
      ..removeListener(_update)
      ..dispose();
55
    WidgetsBinding.instance.removeObserver(this);
Hixie's avatar
Hixie committed
56 57
    super.dispose();
  }
58

59 60 61 62 63 64 65
  @override
  void didChangeMetrics() {
    setState(() {
      // The root transform may have changed, we have to repaint.
    });
  }

Hixie's avatar
Hixie committed
66
  void _update() {
67
    SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
68 69 70 71 72 73 74
      // Semantic information are only available at the end of a frame and our
      // only chance to paint them on the screen is the next frame. To achieve
      // this, we call setState() in a post-frame callback. THIS PATTERN SHOULD
      // NOT BE COPIED. Calling setState() in a post-frame callback is a bad
      // idea as it will not schedule a frame and your app may be lagging behind
      // by one frame. We manually call scheduleFrame() to force a frame and
      // ensure that the semantic information are always painted on the screen.
75 76 77 78 79 80 81
      if (mounted) {
        // If we got disposed this frame, we will still get an update,
        // because the inactive list is flushed after the semantics updates
        // are transmitted to the semantics clients.
        setState(() {
          // The generation of the _SemanticsDebuggerListener has changed.
        });
82
        SchedulerBinding.instance.scheduleFrame();
83
      }
Hixie's avatar
Hixie committed
84 85
    });
  }
86

87
  Offset _lastPointerDownLocation;
Hixie's avatar
Hixie committed
88 89
  void _handlePointerDown(PointerDownEvent event) {
    setState(() {
90
      _lastPointerDownLocation = event.position * ui.window.devicePixelRatio;
Hixie's avatar
Hixie committed
91
    });
92 93
    // TODO(ianh): Use a gesture recognizer so that we can reset the
    // _lastPointerDownLocation when none of the other gesture recognizers win.
Hixie's avatar
Hixie committed
94
  }
95

Hixie's avatar
Hixie committed
96 97
  void _handleTap() {
    assert(_lastPointerDownLocation != null);
98
    _performAction(_lastPointerDownLocation, SemanticsAction.tap);
Hixie's avatar
Hixie committed
99 100 101 102
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
103

Hixie's avatar
Hixie committed
104 105
  void _handleLongPress() {
    assert(_lastPointerDownLocation != null);
106
    _performAction(_lastPointerDownLocation, SemanticsAction.longPress);
Hixie's avatar
Hixie committed
107 108 109 110
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
111

112
  void _handlePanEnd(DragEndDetails details) {
113 114
    final double vx = details.velocity.pixelsPerSecond.dx;
    final double vy = details.velocity.pixelsPerSecond.dy;
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
    if (vx.abs() == vy.abs())
      return;
    if (vx.abs() > vy.abs()) {
      if (vx.sign < 0) {
        _performAction(_lastPointerDownLocation, SemanticsAction.decrease);
        _performAction(_lastPointerDownLocation, SemanticsAction.scrollLeft);
      } else {
        _performAction(_lastPointerDownLocation, SemanticsAction.increase);
        _performAction(_lastPointerDownLocation, SemanticsAction.scrollRight);
      }
    } else {
      if (vy.sign < 0)
        _performAction(_lastPointerDownLocation, SemanticsAction.scrollUp);
      else
        _performAction(_lastPointerDownLocation, SemanticsAction.scrollDown);
    }
Hixie's avatar
Hixie committed
131 132 133 134
    setState(() {
      _lastPointerDownLocation = null;
    });
  }
135

136
  void _performAction(Offset position, SemanticsAction action) {
137 138 139 140 141 142 143
    _pipelineOwner.semanticsOwner?.performActionAt(position, action);
  }

  // TODO(abarth): This shouldn't be a static. We should get the pipeline owner
  // from [context] somehow.
  PipelineOwner get _pipelineOwner => WidgetsBinding.instance.pipelineOwner;

144
  @override
Hixie's avatar
Hixie committed
145 146
  Widget build(BuildContext context) {
    return new CustomPaint(
147 148 149
      foregroundPainter: new _SemanticsDebuggerPainter(
        _pipelineOwner,
        _client.generation,
150 151
        _lastPointerDownLocation, // in physical pixels
        ui.window.devicePixelRatio,
152
      ),
Hixie's avatar
Hixie committed
153 154 155 156 157 158 159 160 161 162 163
      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,
164 165 166 167
            child: widget.child,
          ),
        ),
      ),
Hixie's avatar
Hixie committed
168 169 170 171
    );
  }
}

172 173
class _SemanticsClient extends ChangeNotifier {
  _SemanticsClient(PipelineOwner pipelineOwner) {
174 175 176
    _semanticsHandle = pipelineOwner.ensureSemantics(
      listener: _didUpdateSemantics
    );
Hixie's avatar
Hixie committed
177 178
  }

179
  SemanticsHandle _semanticsHandle;
180 181 182

  @override
  void dispose() {
183 184
    _semanticsHandle.dispose();
    _semanticsHandle = null;
185 186
    super.dispose();
  }
Hixie's avatar
Hixie committed
187 188 189

  int generation = 0;

190
  void _didUpdateSemantics() {
Hixie's avatar
Hixie committed
191
    generation += 1;
192
    notifyListeners();
Hixie's avatar
Hixie committed
193
  }
194 195 196
}

String _getMessage(SemanticsNode node) {
197 198
  final SemanticsData data = node.getSemanticsData();
  final List<String> annotations = <String>[];
Hixie's avatar
Hixie committed
199

200 201 202 203
  bool wantsTap = false;
  if (data.hasFlag(SemanticsFlags.hasCheckedState)) {
    annotations.add(data.hasFlag(SemanticsFlags.isChecked) ? 'checked' : 'unchecked');
    wantsTap = true;
Hixie's avatar
Hixie committed
204 205
  }

206 207 208 209 210 211
  if (data.hasAction(SemanticsAction.tap)) {
    if (!wantsTap)
      annotations.add('button');
  } else {
    if (wantsTap)
      annotations.add('disabled');
Hixie's avatar
Hixie committed
212
  }
213

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230
  if (data.hasAction(SemanticsAction.longPress))
    annotations.add('long-pressable');

  final bool isScrollable = data.hasAction(SemanticsAction.scrollLeft)
                         || data.hasAction(SemanticsAction.scrollRight)
                         || data.hasAction(SemanticsAction.scrollUp)
                         || data.hasAction(SemanticsAction.scrollDown);

  final bool isAdjustable = data.hasAction(SemanticsAction.increase)
                         || data.hasAction(SemanticsAction.decrease);

  if (isScrollable)
    annotations.add('scrollable');

  if (isAdjustable)
    annotations.add('adjustable');

Ian Hickson's avatar
Ian Hickson committed
231
  assert(data.label != null);
232
  String message;
Ian Hickson's avatar
Ian Hickson committed
233 234
  if (data.label.isEmpty) {
    message = annotations.join('; ');
235
  } else {
Ian Hickson's avatar
Ian Hickson committed
236 237 238 239
    String label;
    if (data.textDirection == null) {
      label = '${Unicode.FSI}${data.label}${Unicode.PDI}';
      annotations.insert(0, 'MISSING TEXT DIRECTION');
Hixie's avatar
Hixie committed
240
    } else {
Ian Hickson's avatar
Ian Hickson committed
241 242 243 244 245 246 247 248 249 250 251 252 253
      switch (data.textDirection) {
        case TextDirection.rtl:
          label = '${Unicode.RLI}${data.label}${Unicode.PDF}';
          break;
        case TextDirection.ltr:
          label = data.label;
          break;
      }
    }
    if (annotations.isEmpty) {
      message = label;
    } else {
      message = '$label (${annotations.join('; ')})';
Hixie's avatar
Hixie committed
254 255
    }
  }
256 257 258 259 260 261 262 263 264 265 266

  return message.trim();
}

const TextStyle _messageStyle = const TextStyle(
  color: const Color(0xFF000000),
  fontSize: 10.0,
  height: 0.8
);

void _paintMessage(Canvas canvas, SemanticsNode node) {
267
  final String message = _getMessage(node);
268 269 270 271 272
  if (message.isEmpty)
    return;
  final Rect rect = node.rect;
  canvas.save();
  canvas.clipRect(rect);
273
  final TextPainter textPainter = new TextPainter()
Ian Hickson's avatar
Ian Hickson committed
274 275 276 277 278
    ..text = new TextSpan(
      style: _messageStyle,
      text: message,
    )
    ..textDirection = TextDirection.ltr // _getMessage always returns LTR text, even if node.label is RTL
279
    ..textAlign = TextAlign.center
280 281
    ..layout(maxWidth: rect.width);

282
  textPainter.paint(canvas, Alignment.center.inscribe(textPainter.size, rect).topLeft);
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
  canvas.restore();
}

int _findDepth(SemanticsNode node) {
  if (!node.hasChildren || node.mergeAllDescendantsIntoThisNode)
    return 1;
  int childrenDepth = 0;
  node.visitChildren((SemanticsNode child) {
    childrenDepth = math.max(childrenDepth, _findDepth(child));
    return true;
  });
  return childrenDepth + 1;
}

void _paint(Canvas canvas, SemanticsNode node, int rank) {
  canvas.save();
  if (node.transform != null)
    canvas.transform(node.transform.storage);
301
  final Rect rect = node.rect;
302
  if (!rect.isEmpty) {
303 304
    final Color lineColor = new Color(0xFF000000 + new math.Random(node.id).nextInt(0xFFFFFF));
    final Rect innerRect = rect.deflate(rank * 1.0);
305
    if (innerRect.isEmpty) {
306
      final Paint fill = new Paint()
307 308 309 310
       ..color = lineColor
       ..style = PaintingStyle.fill;
      canvas.drawRect(rect, fill);
    } else {
311
      final Paint fill = new Paint()
312 313 314
       ..color = const Color(0xFFFFFFFF)
       ..style = PaintingStyle.fill;
      canvas.drawRect(rect, fill);
315
      final Paint line = new Paint()
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
       ..strokeWidth = rank * 2.0
       ..color = lineColor
       ..style = PaintingStyle.stroke;
      canvas.drawRect(innerRect, line);
    }
    _paintMessage(canvas, node);
  }
  if (!node.mergeAllDescendantsIntoThisNode) {
    final int childRank = rank - 1;
    node.visitChildren((SemanticsNode child) {
      _paint(canvas, child, childRank);
      return true;
    });
  }
  canvas.restore();
Hixie's avatar
Hixie committed
331 332 333
}

class _SemanticsDebuggerPainter extends CustomPainter {
334
  const _SemanticsDebuggerPainter(this.owner, this.generation, this.pointerPosition, this.devicePixelRatio);
335

336
  final PipelineOwner owner;
Hixie's avatar
Hixie committed
337
  final int generation;
338 339
  final Offset pointerPosition; // in physical pixels
  final double devicePixelRatio;
340

341 342 343 344
  SemanticsNode get _rootSemanticsNode {
    return owner.semanticsOwner?.rootSemanticsNode;
  }

345
  @override
Hixie's avatar
Hixie committed
346
  void paint(Canvas canvas, Size size) {
347
    final SemanticsNode rootNode = _rootSemanticsNode;
348 349
    canvas.save();
    canvas.scale(1.0 / devicePixelRatio, 1.0 / devicePixelRatio);
350 351
    if (rootNode != null)
      _paint(canvas, rootNode, _findDepth(rootNode));
Hixie's avatar
Hixie committed
352
    if (pointerPosition != null) {
353
      final Paint paint = new Paint();
Hixie's avatar
Hixie committed
354
      paint.color = const Color(0x7F0090FF);
355
      canvas.drawCircle(pointerPosition, 10.0 * devicePixelRatio, paint);
Hixie's avatar
Hixie committed
356
    }
357
    canvas.restore();
Hixie's avatar
Hixie committed
358
  }
359 360

  @override
Hixie's avatar
Hixie committed
361
  bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
362 363
    return owner != oldDelegate.owner
        || generation != oldDelegate.generation
Hixie's avatar
Hixie committed
364 365 366
        || pointerPosition != oldDelegate.pointerPosition;
  }
}