1// Copyright 2017 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6import 'dart:convert'; 7import 'dart:developer' as developer; 8import 'dart:math' as math; 9import 'dart:typed_data'; 10import 'dart:ui' as ui 11 show 12 ClipOp, 13 Image, 14 ImageByteFormat, 15 Paragraph, 16 Picture, 17 PictureRecorder, 18 PointMode, 19 SceneBuilder, 20 Vertices; 21import 'dart:ui' show Canvas, Offset; 22 23import 'package:flutter/foundation.dart'; 24import 'package:flutter/painting.dart'; 25import 'package:flutter/rendering.dart'; 26import 'package:flutter/scheduler.dart'; 27import 'package:vector_math/vector_math_64.dart'; 28 29import 'app.dart'; 30import 'basic.dart'; 31import 'binding.dart'; 32import 'debug.dart'; 33import 'framework.dart'; 34import 'gesture_detector.dart'; 35 36/// Signature for the builder callback used by 37/// [WidgetInspector.selectButtonBuilder]. 38typedef InspectorSelectButtonBuilder = Widget Function(BuildContext context, VoidCallback onPressed); 39 40typedef _RegisterServiceExtensionCallback = void Function({ 41 @required String name, 42 @required ServiceExtensionCallback callback, 43}); 44 45/// A layer that mimics the behavior of another layer. 46/// 47/// A proxy layer is used for cases where a layer needs to be placed into 48/// multiple trees of layers. 49class _ProxyLayer extends Layer { 50 _ProxyLayer(this._layer); 51 52 final Layer _layer; 53 54 @override 55 void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { 56 _layer.addToScene(builder, layerOffset); 57 } 58 59 @override 60 S find<S>(Offset regionOffset) => _layer.find(regionOffset); 61 62 @override 63 Iterable<S> findAll<S>(Offset regionOffset) => <S>[]; 64} 65 66/// A [Canvas] that multicasts all method calls to a main canvas and a 67/// secondary screenshot canvas so that a screenshot can be recorded at the same 68/// time as performing a normal paint. 69class _MulticastCanvas implements Canvas { 70 _MulticastCanvas({ 71 @required Canvas main, 72 @required Canvas screenshot, 73 }) : assert(main != null), 74 assert(screenshot != null), 75 _main = main, 76 _screenshot = screenshot; 77 78 final Canvas _main; 79 final Canvas _screenshot; 80 81 @override 82 void clipPath(Path path, { bool doAntiAlias = true }) { 83 _main.clipPath(path, doAntiAlias: doAntiAlias); 84 _screenshot.clipPath(path, doAntiAlias: doAntiAlias); 85 } 86 87 @override 88 void clipRRect(RRect rrect, { bool doAntiAlias = true }) { 89 _main.clipRRect(rrect, doAntiAlias: doAntiAlias); 90 _screenshot.clipRRect(rrect, doAntiAlias: doAntiAlias); 91 } 92 93 @override 94 void clipRect(Rect rect, { ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true }) { 95 _main.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias); 96 _screenshot.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias); 97 } 98 99 @override 100 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) { 101 _main.drawArc(rect, startAngle, sweepAngle, useCenter, paint); 102 _screenshot.drawArc(rect, startAngle, sweepAngle, useCenter, paint); 103 } 104 105 @override 106 void drawAtlas(ui.Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color> colors, BlendMode blendMode, Rect cullRect, Paint paint) { 107 _main.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint); 108 _screenshot.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint); 109 } 110 111 @override 112 void drawCircle(Offset c, double radius, Paint paint) { 113 _main.drawCircle(c, radius, paint); 114 _screenshot.drawCircle(c, radius, paint); 115 } 116 117 @override 118 void drawColor(Color color, BlendMode blendMode) { 119 _main.drawColor(color, blendMode); 120 _screenshot.drawColor(color, blendMode); 121 } 122 123 @override 124 void drawDRRect(RRect outer, RRect inner, Paint paint) { 125 _main.drawDRRect(outer, inner, paint); 126 _screenshot.drawDRRect(outer, inner, paint); 127 } 128 129 @override 130 void drawImage(ui.Image image, Offset p, Paint paint) { 131 _main.drawImage(image, p, paint); 132 _screenshot.drawImage(image, p, paint); 133 } 134 135 @override 136 void drawImageNine(ui.Image image, Rect center, Rect dst, Paint paint) { 137 _main.drawImageNine(image, center, dst, paint); 138 _screenshot.drawImageNine(image, center, dst, paint); 139 } 140 141 @override 142 void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) { 143 _main.drawImageRect(image, src, dst, paint); 144 _screenshot.drawImageRect(image, src, dst, paint); 145 } 146 147 @override 148 void drawLine(Offset p1, Offset p2, Paint paint) { 149 _main.drawLine(p1, p2, paint); 150 _screenshot.drawLine(p1, p2, paint); 151 } 152 153 @override 154 void drawOval(Rect rect, Paint paint) { 155 _main.drawOval(rect, paint); 156 _screenshot.drawOval(rect, paint); 157 } 158 159 @override 160 void drawPaint(Paint paint) { 161 _main.drawPaint(paint); 162 _screenshot.drawPaint(paint); 163 } 164 165 @override 166 void drawParagraph(ui.Paragraph paragraph, Offset offset) { 167 _main.drawParagraph(paragraph, offset); 168 _screenshot.drawParagraph(paragraph, offset); 169 } 170 171 @override 172 void drawPath(Path path, Paint paint) { 173 _main.drawPath(path, paint); 174 _screenshot.drawPath(path, paint); 175 } 176 177 @override 178 void drawPicture(ui.Picture picture) { 179 _main.drawPicture(picture); 180 _screenshot.drawPicture(picture); 181 } 182 183 @override 184 void drawPoints(ui.PointMode pointMode, List<Offset> points, Paint paint) { 185 _main.drawPoints(pointMode, points, paint); 186 _screenshot.drawPoints(pointMode, points, paint); 187 } 188 189 @override 190 void drawRRect(RRect rrect, Paint paint) { 191 _main.drawRRect(rrect, paint); 192 _screenshot.drawRRect(rrect, paint); 193 } 194 195 @override 196 void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List colors, BlendMode blendMode, Rect cullRect, Paint paint) { 197 _main.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint); 198 _screenshot.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint); 199 } 200 201 @override 202 void drawRawPoints(ui.PointMode pointMode, Float32List points, Paint paint) { 203 _main.drawRawPoints(pointMode, points, paint); 204 _screenshot.drawRawPoints(pointMode, points, paint); 205 } 206 207 @override 208 void drawRect(Rect rect, Paint paint) { 209 _main.drawRect(rect, paint); 210 _screenshot.drawRect(rect, paint); 211 } 212 213 @override 214 void drawShadow(Path path, Color color, double elevation, bool transparentOccluder) { 215 _main.drawShadow(path, color, elevation, transparentOccluder); 216 _screenshot.drawShadow(path, color, elevation, transparentOccluder); 217 } 218 219 @override 220 void drawVertices(ui.Vertices vertices, BlendMode blendMode, Paint paint) { 221 _main.drawVertices(vertices, blendMode, paint); 222 _screenshot.drawVertices(vertices, blendMode, paint); 223 } 224 225 @override 226 int getSaveCount() { 227 // The main canvas is used instead of the screenshot canvas as the main 228 // canvas is guaranteed to be consistent with the canvas expected by the 229 // normal paint pipeline so any logic depending on getSaveCount() will 230 // behave the same as for the regular paint pipeline. 231 return _main.getSaveCount(); 232 } 233 234 @override 235 void restore() { 236 _main.restore(); 237 _screenshot.restore(); 238 } 239 240 @override 241 void rotate(double radians) { 242 _main.rotate(radians); 243 _screenshot.rotate(radians); 244 } 245 246 @override 247 void save() { 248 _main.save(); 249 _screenshot.save(); 250 } 251 252 @override 253 void saveLayer(Rect bounds, Paint paint) { 254 _main.saveLayer(bounds, paint); 255 _screenshot.saveLayer(bounds, paint); 256 } 257 258 @override 259 void scale(double sx, [ double sy ]) { 260 _main.scale(sx, sy); 261 _screenshot.scale(sx, sy); 262 } 263 264 @override 265 void skew(double sx, double sy) { 266 _main.skew(sx, sy); 267 _screenshot.skew(sx, sy); 268 } 269 270 @override 271 void transform(Float64List matrix4) { 272 _main.transform(matrix4); 273 _screenshot.transform(matrix4); 274 } 275 276 @override 277 void translate(double dx, double dy) { 278 _main.translate(dx, dy); 279 _screenshot.translate(dx, dy); 280 } 281} 282 283Rect _calculateSubtreeBoundsHelper(RenderObject object, Matrix4 transform) { 284 Rect bounds = MatrixUtils.transformRect(transform, object.semanticBounds); 285 286 object.visitChildren((RenderObject child) { 287 final Matrix4 childTransform = transform.clone(); 288 object.applyPaintTransform(child, childTransform); 289 Rect childBounds = _calculateSubtreeBoundsHelper(child, childTransform); 290 final Rect paintClip = object.describeApproximatePaintClip(child); 291 if (paintClip != null) { 292 final Rect transformedPaintClip = MatrixUtils.transformRect( 293 transform, 294 paintClip, 295 ); 296 childBounds = childBounds.intersect(transformedPaintClip); 297 } 298 299 if (childBounds.isFinite && !childBounds.isEmpty) { 300 bounds = bounds.isEmpty ? childBounds : bounds.expandToInclude(childBounds); 301 } 302 }); 303 304 return bounds; 305} 306 307/// Calculate bounds for a render object and all of its descendants. 308Rect _calculateSubtreeBounds(RenderObject object) { 309 return _calculateSubtreeBoundsHelper(object, Matrix4.identity()); 310} 311 312/// A layer that omits its own offset when adding children to the scene so that 313/// screenshots render to the scene in the local coordinate system of the layer. 314class _ScreenshotContainerLayer extends OffsetLayer { 315 @override 316 void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { 317 addChildrenToScene(builder, layerOffset); 318 } 319} 320 321/// Data shared between nested [_ScreenshotPaintingContext] objects recording 322/// a screenshot. 323class _ScreenshotData { 324 _ScreenshotData({ 325 @required this.target, 326 }) : assert(target != null), 327 containerLayer = _ScreenshotContainerLayer(); 328 329 /// Target to take a screenshot of. 330 final RenderObject target; 331 332 /// Root of the layer tree containing the screenshot. 333 final OffsetLayer containerLayer; 334 335 /// Whether the screenshot target has already been found in the render tree. 336 bool foundTarget = false; 337 338 /// Whether paint operations should record to the screenshot. 339 /// 340 /// At least one of [includeInScreenshot] and [includeInRegularContext] must 341 /// be true. 342 bool includeInScreenshot = false; 343 344 /// Whether paint operations should record to the regular context. 345 /// 346 /// This should only be set to false before paint operations that should only 347 /// apply to the screenshot such rendering debug information about the 348 /// [target]. 349 /// 350 /// At least one of [includeInScreenshot] and [includeInRegularContext] must 351 /// be true. 352 bool includeInRegularContext = true; 353 354 /// Offset of the screenshot corresponding to the offset [target] was given as 355 /// part of the regular paint. 356 Offset get screenshotOffset { 357 assert(foundTarget); 358 return containerLayer.offset; 359 } 360 set screenshotOffset(Offset offset) { 361 containerLayer.offset = offset; 362 } 363} 364 365/// A place to paint to build screenshots of [RenderObject]s. 366/// 367/// Requires that the render objects have already painted successfully as part 368/// of the regular rendering pipeline. 369/// This painting context behaves the same as standard [PaintingContext] with 370/// instrumentation added to compute a screenshot of a specified [RenderObject] 371/// added. To correctly mimic the behavior of the regular rendering pipeline, the 372/// full subtree of the first [RepaintBoundary] ancestor of the specified 373/// [RenderObject] will also be rendered rather than just the subtree of the 374/// render object. 375class _ScreenshotPaintingContext extends PaintingContext { 376 _ScreenshotPaintingContext({ 377 @required ContainerLayer containerLayer, 378 @required Rect estimatedBounds, 379 @required _ScreenshotData screenshotData, 380 }) : _data = screenshotData, 381 super(containerLayer, estimatedBounds); 382 383 final _ScreenshotData _data; 384 385 // Recording state 386 PictureLayer _screenshotCurrentLayer; 387 ui.PictureRecorder _screenshotRecorder; 388 Canvas _screenshotCanvas; 389 _MulticastCanvas _multicastCanvas; 390 391 @override 392 Canvas get canvas { 393 if (_data.includeInScreenshot) { 394 if (_screenshotCanvas == null) { 395 _startRecordingScreenshot(); 396 } 397 assert(_screenshotCanvas != null); 398 return _data.includeInRegularContext ? _multicastCanvas : _screenshotCanvas; 399 } else { 400 assert(_data.includeInRegularContext); 401 return super.canvas; 402 } 403 } 404 405 bool get _isScreenshotRecording { 406 final bool hasScreenshotCanvas = _screenshotCanvas != null; 407 assert(() { 408 if (hasScreenshotCanvas) { 409 assert(_screenshotCurrentLayer != null); 410 assert(_screenshotRecorder != null); 411 assert(_screenshotCanvas != null); 412 } else { 413 assert(_screenshotCurrentLayer == null); 414 assert(_screenshotRecorder == null); 415 assert(_screenshotCanvas == null); 416 } 417 return true; 418 }()); 419 return hasScreenshotCanvas; 420 } 421 422 void _startRecordingScreenshot() { 423 assert(_data.includeInScreenshot); 424 assert(!_isScreenshotRecording); 425 _screenshotCurrentLayer = PictureLayer(estimatedBounds); 426 _screenshotRecorder = ui.PictureRecorder(); 427 _screenshotCanvas = Canvas(_screenshotRecorder); 428 _data.containerLayer.append(_screenshotCurrentLayer); 429 if (_data.includeInRegularContext) { 430 _multicastCanvas = _MulticastCanvas( 431 main: super.canvas, 432 screenshot: _screenshotCanvas, 433 ); 434 } else { 435 _multicastCanvas = null; 436 } 437 } 438 439 @override 440 void stopRecordingIfNeeded() { 441 super.stopRecordingIfNeeded(); 442 _stopRecordingScreenshotIfNeeded(); 443 } 444 445 void _stopRecordingScreenshotIfNeeded() { 446 if (!_isScreenshotRecording) 447 return; 448 // There is no need to ever draw repaint rainbows as part of the screenshot. 449 _screenshotCurrentLayer.picture = _screenshotRecorder.endRecording(); 450 _screenshotCurrentLayer = null; 451 _screenshotRecorder = null; 452 _multicastCanvas = null; 453 _screenshotCanvas = null; 454 } 455 456 @override 457 void appendLayer(Layer layer) { 458 if (_data.includeInRegularContext) { 459 super.appendLayer(layer); 460 if (_data.includeInScreenshot) { 461 assert(!_isScreenshotRecording); 462 // We must use a proxy layer here as the layer is already attached to 463 // the regular layer tree. 464 _data.containerLayer.append(_ProxyLayer(layer)); 465 } 466 } else { 467 // Only record to the screenshot. 468 assert(!_isScreenshotRecording); 469 assert(_data.includeInScreenshot); 470 layer.remove(); 471 _data.containerLayer.append(layer); 472 return; 473 } 474 } 475 476 @override 477 PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) { 478 if (_data.foundTarget) { 479 // We have already found the screenshotTarget in the layer tree 480 // so we can optimize and use a standard PaintingContext. 481 return super.createChildContext(childLayer, bounds); 482 } else { 483 return _ScreenshotPaintingContext( 484 containerLayer: childLayer, 485 estimatedBounds: bounds, 486 screenshotData: _data, 487 ); 488 } 489 } 490 491 @override 492 void paintChild(RenderObject child, Offset offset) { 493 final bool isScreenshotTarget = identical(child, _data.target); 494 if (isScreenshotTarget) { 495 assert(!_data.includeInScreenshot); 496 assert(!_data.foundTarget); 497 _data.foundTarget = true; 498 _data.screenshotOffset = offset; 499 _data.includeInScreenshot = true; 500 } 501 super.paintChild(child, offset); 502 if (isScreenshotTarget) { 503 _stopRecordingScreenshotIfNeeded(); 504 _data.includeInScreenshot = false; 505 } 506 } 507 508 /// Captures an image of the current state of [renderObject] and its children. 509 /// 510 /// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset 511 /// by the top-left corner of [renderBounds], and have dimensions equal to the 512 /// size of [renderBounds] multiplied by [pixelRatio]. 513 /// 514 /// To use [toImage], the render object must have gone through the paint phase 515 /// (i.e. [debugNeedsPaint] must be false). 516 /// 517 /// The [pixelRatio] describes the scale between the logical pixels and the 518 /// size of the output image. It is independent of the 519 /// [window.devicePixelRatio] for the device, so specifying 1.0 (the default) 520 /// will give you a 1:1 mapping between logical pixels and the output pixels 521 /// in the image. 522 /// 523 /// The [debugPaint] argument specifies whether the image should include the 524 /// output of [RenderObject.debugPaint] for [renderObject] with 525 /// [debugPaintSizeEnabled] set to true. Debug paint information is not 526 /// included for the children of [renderObject] so that it is clear precisely 527 /// which object the debug paint information references. 528 /// 529 /// See also: 530 /// 531 /// * [RenderRepaintBoundary.toImage] for a similar API for [RenderObject]s 532 /// that are repaint boundaries that can be used outside of the inspector. 533 /// * [OffsetLayer.toImage] for a similar API at the layer level. 534 /// * [dart:ui.Scene.toImage] for more information about the image returned. 535 static Future<ui.Image> toImage( 536 RenderObject renderObject, 537 Rect renderBounds, { 538 double pixelRatio = 1.0, 539 bool debugPaint = false, 540 }) { 541 RenderObject repaintBoundary = renderObject; 542 while (repaintBoundary != null && !repaintBoundary.isRepaintBoundary) { 543 repaintBoundary = repaintBoundary.parent; 544 } 545 assert(repaintBoundary != null); 546 final _ScreenshotData data = _ScreenshotData(target: renderObject); 547 final _ScreenshotPaintingContext context = _ScreenshotPaintingContext( 548 containerLayer: repaintBoundary.debugLayer, 549 estimatedBounds: repaintBoundary.paintBounds, 550 screenshotData: data, 551 ); 552 553 if (identical(renderObject, repaintBoundary)) { 554 // Painting the existing repaint boundary to the screenshot is sufficient. 555 // We don't just take a direct screenshot of the repaint boundary as we 556 // want to capture debugPaint information as well. 557 data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer)); 558 data.foundTarget = true; 559 final OffsetLayer offsetLayer = repaintBoundary.debugLayer; 560 data.screenshotOffset = offsetLayer.offset; 561 } else { 562 // Repaint everything under the repaint boundary. 563 // We call debugInstrumentRepaintCompositedChild instead of paintChild as 564 // we need to force everything under the repaint boundary to repaint. 565 PaintingContext.debugInstrumentRepaintCompositedChild( 566 repaintBoundary, 567 customContext: context, 568 ); 569 } 570 571 // The check that debugPaintSizeEnabled is false exists to ensure we only 572 // call debugPaint when it wasn't already called. 573 if (debugPaint && !debugPaintSizeEnabled) { 574 data.includeInRegularContext = false; 575 // Existing recording may be to a canvas that draws to both the normal and 576 // screenshot canvases. 577 context.stopRecordingIfNeeded(); 578 assert(data.foundTarget); 579 data.includeInScreenshot = true; 580 581 debugPaintSizeEnabled = true; 582 try { 583 renderObject.debugPaint(context, data.screenshotOffset); 584 } finally { 585 debugPaintSizeEnabled = false; 586 context.stopRecordingIfNeeded(); 587 } 588 } 589 590 // We must build the regular scene before we can build the screenshot 591 // scene as building the screenshot scene assumes addToScene has already 592 // been called successfully for all layers in the regular scene. 593 repaintBoundary.debugLayer.buildScene(ui.SceneBuilder()); 594 595 return data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio); 596 } 597} 598 599/// A class describing a step along a path through a tree of [DiagnosticsNode] 600/// objects. 601/// 602/// This class is used to bundle all data required to display the tree with just 603/// the nodes along a path expanded into a single JSON payload. 604class _DiagnosticsPathNode { 605 /// Creates a full description of a step in a path through a tree of 606 /// [DiagnosticsNode] objects. 607 /// 608 /// The [node] and [child] arguments must not be null. 609 _DiagnosticsPathNode({ 610 @required this.node, 611 @required this.children, 612 this.childIndex, 613 }) : assert(node != null), 614 assert(children != null); 615 616 /// Node at the point in the path this [_DiagnosticsPathNode] is describing. 617 final DiagnosticsNode node; 618 619 /// Children of the [node] being described. 620 /// 621 /// This value is cached instead of relying on `node.getChildren()` as that 622 /// method call might create new [DiagnosticsNode] objects for each child 623 /// and we would prefer to use the identical [DiagnosticsNode] for each time 624 /// a node exists in the path. 625 final List<DiagnosticsNode> children; 626 627 /// Index of the child that the path continues on. 628 /// 629 /// Equal to null if the path does not continue. 630 final int childIndex; 631} 632 633List<_DiagnosticsPathNode> _followDiagnosticableChain( 634 List<Diagnosticable> chain, { 635 String name, 636 DiagnosticsTreeStyle style, 637}) { 638 final List<_DiagnosticsPathNode> path = <_DiagnosticsPathNode>[]; 639 if (chain.isEmpty) 640 return path; 641 DiagnosticsNode diagnostic = chain.first.toDiagnosticsNode(name: name, style: style); 642 for (int i = 1; i < chain.length; i += 1) { 643 final Diagnosticable target = chain[i]; 644 bool foundMatch = false; 645 final List<DiagnosticsNode> children = diagnostic.getChildren(); 646 for (int j = 0; j < children.length; j += 1) { 647 final DiagnosticsNode child = children[j]; 648 if (child.value == target) { 649 foundMatch = true; 650 path.add(_DiagnosticsPathNode( 651 node: diagnostic, 652 children: children, 653 childIndex: j, 654 )); 655 diagnostic = child; 656 break; 657 } 658 } 659 assert(foundMatch); 660 } 661 path.add(_DiagnosticsPathNode(node: diagnostic, children: diagnostic.getChildren())); 662 return path; 663} 664 665/// Signature for the selection change callback used by 666/// [WidgetInspectorService.selectionChangedCallback]. 667typedef InspectorSelectionChangedCallback = void Function(); 668 669/// Structure to help reference count Dart objects referenced by a GUI tool 670/// using [WidgetInspectorService]. 671class _InspectorReferenceData { 672 _InspectorReferenceData(this.object); 673 674 final Object object; 675 int count = 1; 676} 677 678// Production implementation of [WidgetInspectorService]. 679class _WidgetInspectorService = Object with WidgetInspectorService; 680 681/// Service used by GUI tools to interact with the [WidgetInspector]. 682/// 683/// Calls to this object are typically made from GUI tools such as the [Flutter 684/// IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md) 685/// using the [Dart VM Service protocol](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md). 686/// This class uses its own object id and manages object lifecycles itself 687/// instead of depending on the [object ids](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getobject) 688/// specified by the VM Service Protocol because the VM Service Protocol ids 689/// expire unpredictably. Object references are tracked in groups so that tools 690/// that clients can use dereference all objects in a group with a single 691/// operation making it easier to avoid memory leaks. 692/// 693/// All methods in this class are appropriate to invoke from debugging tools 694/// using the Observatory service protocol to evaluate Dart expressions of the 695/// form `WidgetInspectorService.instance.methodName(arg1, arg2, ...)`. If you 696/// make changes to any instance method of this class you need to verify that 697/// the [Flutter IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md) 698/// widget inspector support still works with the changes. 699/// 700/// All methods returning String values return JSON. 701mixin WidgetInspectorService { 702 /// Ring of cached JSON values to prevent json from being garbage 703 /// collected before it can be requested over the Observatory protocol. 704 final List<String> _serializeRing = List<String>(20); 705 int _serializeRingIndex = 0; 706 707 /// The current [WidgetInspectorService]. 708 static WidgetInspectorService get instance => _instance; 709 static WidgetInspectorService _instance = _WidgetInspectorService(); 710 @protected 711 static set instance(WidgetInspectorService instance) { 712 _instance = instance; 713 } 714 715 static bool _debugServiceExtensionsRegistered = false; 716 717 /// Ground truth tracking what object(s) are currently selected used by both 718 /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector] 719 /// displayed on the device. 720 final InspectorSelection selection = InspectorSelection(); 721 722 /// Callback typically registered by the [WidgetInspector] to receive 723 /// notifications when [selection] changes. 724 /// 725 /// The Flutter IntelliJ Plugin does not need to listen for this event as it 726 /// instead listens for `dart:developer` `inspect` events which also trigger 727 /// when the inspection target changes on device. 728 InspectorSelectionChangedCallback selectionChangedCallback; 729 730 /// The Observatory protocol does not keep alive object references so this 731 /// class needs to manually manage groups of objects that should be kept 732 /// alive. 733 final Map<String, Set<_InspectorReferenceData>> _groups = <String, Set<_InspectorReferenceData>>{}; 734 final Map<String, _InspectorReferenceData> _idToReferenceData = <String, _InspectorReferenceData>{}; 735 final Map<Object, String> _objectToId = Map<Object, String>.identity(); 736 int _nextId = 0; 737 738 List<String> _pubRootDirectories; 739 740 bool _trackRebuildDirtyWidgets = false; 741 bool _trackRepaintWidgets = false; 742 743 _RegisterServiceExtensionCallback _registerServiceExtensionCallback; 744 /// Registers a service extension method with the given name (full 745 /// name "ext.flutter.inspector.name"). 746 /// 747 /// The given callback is called when the extension method is called. The 748 /// callback must return a value that can be converted to JSON using 749 /// `json.encode()` (see [JsonEncoder]). The return value is stored as a 750 /// property named `result` in the JSON. In case of failure, the failure is 751 /// reported to the remote caller and is dumped to the logs. 752 @protected 753 void registerServiceExtension({ 754 @required String name, 755 @required ServiceExtensionCallback callback, 756 }) { 757 _registerServiceExtensionCallback( 758 name: 'inspector.$name', 759 callback: callback, 760 ); 761 } 762 763 /// Registers a service extension method with the given name (full 764 /// name "ext.flutter.inspector.name"), which takes no arguments. 765 void _registerSignalServiceExtension({ 766 @required String name, 767 @required FutureOr<Object> callback(), 768 }) { 769 registerServiceExtension( 770 name: name, 771 callback: (Map<String, String> parameters) async { 772 return <String, Object>{'result': await callback()}; 773 }, 774 ); 775 } 776 777 /// Registers a service extension method with the given name (full 778 /// name "ext.flutter.inspector.name"), which takes a single optional argument 779 /// "objectGroup" specifying what group is used to manage lifetimes of 780 /// object references in the returned JSON (see [disposeGroup]). 781 /// If "objectGroup" is omitted, the returned JSON will not include any object 782 /// references to avoid leaking memory. 783 void _registerObjectGroupServiceExtension({ 784 @required String name, 785 @required FutureOr<Object> callback(String objectGroup), 786 }) { 787 registerServiceExtension( 788 name: name, 789 callback: (Map<String, String> parameters) async { 790 return <String, Object>{'result': await callback(parameters['objectGroup'])}; 791 }, 792 ); 793 } 794 795 /// Registers a service extension method with the given name (full 796 /// name "ext.flutter.inspector.name"), which takes a single argument 797 /// "enabled" which can have the value "true" or the value "false" 798 /// or can be omitted to read the current value. (Any value other 799 /// than "true" is considered equivalent to "false". Other arguments 800 /// are ignored.) 801 /// 802 /// Calls the `getter` callback to obtain the value when 803 /// responding to the service extension method being called. 804 /// 805 /// Calls the `setter` callback with the new value when the 806 /// service extension method is called with a new value. 807 void _registerBoolServiceExtension({ 808 @required String name, 809 @required AsyncValueGetter<bool> getter, 810 @required AsyncValueSetter<bool> setter, 811 }) { 812 assert(name != null); 813 assert(getter != null); 814 assert(setter != null); 815 registerServiceExtension( 816 name: name, 817 callback: (Map<String, String> parameters) async { 818 if (parameters.containsKey('enabled')) { 819 final bool value = parameters['enabled'] == 'true'; 820 await setter(value); 821 _postExtensionStateChangedEvent(name, value); 822 } 823 return <String, dynamic>{'enabled': await getter() ? 'true' : 'false'}; 824 }, 825 ); 826 } 827 828 /// Sends an event when a service extension's state is changed. 829 /// 830 /// Clients should listen for this event to stay aware of the current service 831 /// extension state. Any service extension that manages a state should call 832 /// this method on state change. 833 /// 834 /// `value` reflects the newly updated service extension value. 835 /// 836 /// This will be called automatically for service extensions registered via 837 /// [registerBoolServiceExtension]. 838 void _postExtensionStateChangedEvent(String name, dynamic value) { 839 postEvent( 840 'Flutter.ServiceExtensionStateChanged', 841 <String, dynamic>{ 842 'extension': 'ext.flutter.inspector.$name', 843 'value': value, 844 }, 845 ); 846 } 847 848 /// Registers a service extension method with the given name (full 849 /// name "ext.flutter.inspector.name") which takes an optional parameter named 850 /// "arg" and a required parameter named "objectGroup" used to control the 851 /// lifetimes of object references in the returned JSON (see [disposeGroup]). 852 void _registerServiceExtensionWithArg({ 853 @required String name, 854 @required FutureOr<Object> callback(String objectId, String objectGroup), 855 }) { 856 registerServiceExtension( 857 name: name, 858 callback: (Map<String, String> parameters) async { 859 assert(parameters.containsKey('objectGroup')); 860 return <String, Object>{ 861 'result': await callback(parameters['arg'], parameters['objectGroup']), 862 }; 863 }, 864 ); 865 } 866 867 /// Registers a service extension method with the given name (full 868 /// name "ext.flutter.inspector.name"), that takes arguments 869 /// "arg0", "arg1", "arg2", ..., "argn". 870 void _registerServiceExtensionVarArgs({ 871 @required String name, 872 @required FutureOr<Object> callback(List<String> args), 873 }) { 874 registerServiceExtension( 875 name: name, 876 callback: (Map<String, String> parameters) async { 877 const String argPrefix = 'arg'; 878 final List<String> args = <String>[]; 879 parameters.forEach((String name, String value) { 880 if (name.startsWith(argPrefix)) { 881 final int index = int.parse(name.substring(argPrefix.length)); 882 if (index >= args.length) { 883 args.length = index + 1; 884 } 885 args[index] = value; 886 } 887 }); 888 return <String, Object>{'result': await callback(args)}; 889 }, 890 ); 891 } 892 893 /// Cause the entire tree to be rebuilt. This is used by development tools 894 /// when the application code has changed and is being hot-reloaded, to cause 895 /// the widget tree to pick up any changed implementations. 896 /// 897 /// This is expensive and should not be called except during development. 898 @protected 899 Future<void> forceRebuild() { 900 final WidgetsBinding binding = WidgetsBinding.instance; 901 if (binding.renderViewElement != null) { 902 binding.buildOwner.reassemble(binding.renderViewElement); 903 return binding.endOfFrame; 904 } 905 return Future<void>.value(); 906 } 907 908 static const String _consoleObjectGroup = 'console-group'; 909 910 int _errorsSinceReload = 0; 911 912 void _reportError(FlutterErrorDetails details) { 913 final Map<String, Object> errorJson = _nodeToJson( 914 details.toDiagnosticsNode(), 915 _SerializationDelegate( 916 groupName: _consoleObjectGroup, 917 subtreeDepth: 5, 918 includeProperties: true, 919 expandPropertyValues: true, 920 maxDescendentsTruncatableNode: 5, 921 service: this, 922 ), 923 ); 924 925 errorJson['errorsSinceReload'] = _errorsSinceReload; 926 _errorsSinceReload += 1; 927 928 postEvent('Flutter.Error', errorJson); 929 } 930 931 /// Resets the count of errors since the last hot reload. 932 /// 933 /// This data is sent to clients as part of the 'Flutter.Error' service 934 /// protocol event. Clients may choose to display errors received after the 935 /// first error differently. 936 void _resetErrorCount() { 937 _errorsSinceReload = 0; 938 } 939 940 /// Called to register service extensions. 941 /// 942 /// See also: 943 /// 944 /// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses> 945 /// * [BindingBase.initServiceExtensions], which explains when service 946 /// extensions can be used. 947 void initServiceExtensions(_RegisterServiceExtensionCallback registerServiceExtensionCallback) { 948 _registerServiceExtensionCallback = registerServiceExtensionCallback; 949 assert(!_debugServiceExtensionsRegistered); 950 assert(() { _debugServiceExtensionsRegistered = true; return true; }()); 951 952 SchedulerBinding.instance.addPersistentFrameCallback(_onFrameStart); 953 954 final FlutterExceptionHandler structuredExceptionHandler = _reportError; 955 final FlutterExceptionHandler defaultExceptionHandler = FlutterError.onError; 956 957 _registerBoolServiceExtension( 958 name: 'structuredErrors', 959 getter: () async => FlutterError.onError == structuredExceptionHandler, 960 setter: (bool value) { 961 FlutterError.onError = value ? structuredExceptionHandler : defaultExceptionHandler; 962 return Future<void>.value(); 963 }, 964 ); 965 966 _registerBoolServiceExtension( 967 name: 'show', 968 getter: () async => WidgetsApp.debugShowWidgetInspectorOverride, 969 setter: (bool value) { 970 if (WidgetsApp.debugShowWidgetInspectorOverride == value) { 971 return Future<void>.value(); 972 } 973 WidgetsApp.debugShowWidgetInspectorOverride = value; 974 return forceRebuild(); 975 }, 976 ); 977 978 if (isWidgetCreationTracked()) { 979 // Service extensions that are only supported if widget creation locations 980 // are tracked. 981 _registerBoolServiceExtension( 982 name: 'trackRebuildDirtyWidgets', 983 getter: () async => _trackRebuildDirtyWidgets, 984 setter: (bool value) async { 985 if (value == _trackRebuildDirtyWidgets) { 986 return; 987 } 988 _rebuildStats.resetCounts(); 989 _trackRebuildDirtyWidgets = value; 990 if (value) { 991 assert(debugOnRebuildDirtyWidget == null); 992 debugOnRebuildDirtyWidget = _onRebuildWidget; 993 // Trigger a rebuild so there are baseline stats for rebuilds 994 // performed by the app. 995 await forceRebuild(); 996 return; 997 } else { 998 debugOnRebuildDirtyWidget = null; 999 return; 1000 } 1001 }, 1002 ); 1003 1004 _registerBoolServiceExtension( 1005 name: 'trackRepaintWidgets', 1006 getter: () async => _trackRepaintWidgets, 1007 setter: (bool value) async { 1008 if (value == _trackRepaintWidgets) { 1009 return; 1010 } 1011 _repaintStats.resetCounts(); 1012 _trackRepaintWidgets = value; 1013 if (value) { 1014 assert(debugOnProfilePaint == null); 1015 debugOnProfilePaint = _onPaint; 1016 // Trigger an immediate paint so the user has some baseline painting 1017 // stats to view. 1018 void markTreeNeedsPaint(RenderObject renderObject) { 1019 renderObject.markNeedsPaint(); 1020 renderObject.visitChildren(markTreeNeedsPaint); 1021 } 1022 final RenderObject root = RendererBinding.instance.renderView; 1023 if (root != null) { 1024 markTreeNeedsPaint(root); 1025 } 1026 } else { 1027 debugOnProfilePaint = null; 1028 } 1029 }, 1030 ); 1031 } 1032 1033 _registerSignalServiceExtension( 1034 name: 'disposeAllGroups', 1035 callback: disposeAllGroups, 1036 ); 1037 _registerObjectGroupServiceExtension( 1038 name: 'disposeGroup', 1039 callback: disposeGroup, 1040 ); 1041 _registerSignalServiceExtension( 1042 name: 'isWidgetTreeReady', 1043 callback: isWidgetTreeReady, 1044 ); 1045 _registerServiceExtensionWithArg( 1046 name: 'disposeId', 1047 callback: disposeId, 1048 ); 1049 _registerServiceExtensionVarArgs( 1050 name: 'setPubRootDirectories', 1051 callback: setPubRootDirectories, 1052 ); 1053 _registerServiceExtensionWithArg( 1054 name: 'setSelectionById', 1055 callback: setSelectionById, 1056 ); 1057 _registerServiceExtensionWithArg( 1058 name: 'getParentChain', 1059 callback: _getParentChain, 1060 ); 1061 _registerServiceExtensionWithArg( 1062 name: 'getProperties', 1063 callback: _getProperties, 1064 ); 1065 _registerServiceExtensionWithArg( 1066 name: 'getChildren', 1067 callback: _getChildren, 1068 ); 1069 1070 _registerServiceExtensionWithArg( 1071 name: 'getChildrenSummaryTree', 1072 callback: _getChildrenSummaryTree, 1073 ); 1074 1075 _registerServiceExtensionWithArg( 1076 name: 'getChildrenDetailsSubtree', 1077 callback: _getChildrenDetailsSubtree, 1078 ); 1079 1080 _registerObjectGroupServiceExtension( 1081 name: 'getRootWidget', 1082 callback: _getRootWidget, 1083 ); 1084 _registerObjectGroupServiceExtension( 1085 name: 'getRootRenderObject', 1086 callback: _getRootRenderObject, 1087 ); 1088 _registerObjectGroupServiceExtension( 1089 name: 'getRootWidgetSummaryTree', 1090 callback: _getRootWidgetSummaryTree, 1091 ); 1092 _registerServiceExtensionWithArg( 1093 name: 'getDetailsSubtree', 1094 callback: _getDetailsSubtree, 1095 ); 1096 _registerServiceExtensionWithArg( 1097 name: 'getSelectedRenderObject', 1098 callback: _getSelectedRenderObject, 1099 ); 1100 _registerServiceExtensionWithArg( 1101 name: 'getSelectedWidget', 1102 callback: _getSelectedWidget, 1103 ); 1104 _registerServiceExtensionWithArg( 1105 name: 'getSelectedSummaryWidget', 1106 callback: _getSelectedSummaryWidget, 1107 ); 1108 1109 _registerSignalServiceExtension( 1110 name: 'isWidgetCreationTracked', 1111 callback: isWidgetCreationTracked, 1112 ); 1113 registerServiceExtension( 1114 name: 'screenshot', 1115 callback: (Map<String, String> parameters) async { 1116 assert(parameters.containsKey('id')); 1117 assert(parameters.containsKey('width')); 1118 assert(parameters.containsKey('height')); 1119 1120 final ui.Image image = await screenshot( 1121 toObject(parameters['id']), 1122 width: double.parse(parameters['width']), 1123 height: double.parse(parameters['height']), 1124 margin: parameters.containsKey('margin') ? 1125 double.parse(parameters['margin']) : 0.0, 1126 maxPixelRatio: parameters.containsKey('maxPixelRatio') ? 1127 double.parse(parameters['maxPixelRatio']) : 1.0, 1128 debugPaint: parameters['debugPaint'] == 'true', 1129 ); 1130 if (image == null) { 1131 return <String, Object>{'result': null}; 1132 } 1133 final ByteData byteData = await image.toByteData(format:ui.ImageByteFormat.png); 1134 1135 return <String, Object>{ 1136 'result': base64.encoder.convert(Uint8List.view(byteData.buffer)), 1137 }; 1138 }, 1139 ); 1140 } 1141 1142 void _clearStats() { 1143 _rebuildStats.resetCounts(); 1144 _repaintStats.resetCounts(); 1145 } 1146 1147 /// Clear all InspectorService object references. 1148 /// 1149 /// Use this method only for testing to ensure that object references from one 1150 /// test case do not impact other test cases. 1151 @protected 1152 void disposeAllGroups() { 1153 _groups.clear(); 1154 _idToReferenceData.clear(); 1155 _objectToId.clear(); 1156 _nextId = 0; 1157 } 1158 1159 /// Free all references to objects in a group. 1160 /// 1161 /// Objects and their associated ids in the group may be kept alive by 1162 /// references from a different group. 1163 @protected 1164 void disposeGroup(String name) { 1165 final Set<_InspectorReferenceData> references = _groups.remove(name); 1166 if (references == null) 1167 return; 1168 references.forEach(_decrementReferenceCount); 1169 } 1170 1171 void _decrementReferenceCount(_InspectorReferenceData reference) { 1172 reference.count -= 1; 1173 assert(reference.count >= 0); 1174 if (reference.count == 0) { 1175 final String id = _objectToId.remove(reference.object); 1176 assert(id != null); 1177 _idToReferenceData.remove(id); 1178 } 1179 } 1180 1181 /// Returns a unique id for [object] that will remain live at least until 1182 /// [disposeGroup] is called on [groupName] or [dispose] is called on the id 1183 /// returned by this method. 1184 @protected 1185 String toId(Object object, String groupName) { 1186 if (object == null) 1187 return null; 1188 1189 final Set<_InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<_InspectorReferenceData>.identity()); 1190 String id = _objectToId[object]; 1191 _InspectorReferenceData referenceData; 1192 if (id == null) { 1193 id = 'inspector-$_nextId'; 1194 _nextId += 1; 1195 _objectToId[object] = id; 1196 referenceData = _InspectorReferenceData(object); 1197 _idToReferenceData[id] = referenceData; 1198 group.add(referenceData); 1199 } else { 1200 referenceData = _idToReferenceData[id]; 1201 if (group.add(referenceData)) 1202 referenceData.count += 1; 1203 } 1204 return id; 1205 } 1206 1207 /// Returns whether the application has rendered its first frame and it is 1208 /// appropriate to display the Widget tree in the inspector. 1209 @protected 1210 bool isWidgetTreeReady([ String groupName ]) { 1211 return WidgetsBinding.instance != null && 1212 WidgetsBinding.instance.debugDidSendFirstFrameEvent; 1213 } 1214 1215 /// Returns the Dart object associated with a reference id. 1216 /// 1217 /// The `groupName` parameter is not required by is added to regularize the 1218 /// API surface of the methods in this class called from the Flutter IntelliJ 1219 /// Plugin. 1220 @protected 1221 Object toObject(String id, [ String groupName ]) { 1222 if (id == null) 1223 return null; 1224 1225 final _InspectorReferenceData data = _idToReferenceData[id]; 1226 if (data == null) { 1227 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]); 1228 } 1229 return data.object; 1230 } 1231 1232 /// Returns the object to introspect to determine the source location of an 1233 /// object's class. 1234 /// 1235 /// The Dart object for the id is returned for all cases but [Element] objects 1236 /// where the [Widget] configuring the [Element] is returned instead as the 1237 /// class of the [Widget] is more relevant than the class of the [Element]. 1238 /// 1239 /// The `groupName` parameter is not required by is added to regularize the 1240 /// API surface of methods called from the Flutter IntelliJ Plugin. 1241 @protected 1242 Object toObjectForSourceLocation(String id, [ String groupName ]) { 1243 final Object object = toObject(id); 1244 if (object is Element) { 1245 return object.widget; 1246 } 1247 return object; 1248 } 1249 1250 /// Remove the object with the specified `id` from the specified object 1251 /// group. 1252 /// 1253 /// If the object exists in other groups it will remain alive and the object 1254 /// id will remain valid. 1255 @protected 1256 void disposeId(String id, String groupName) { 1257 if (id == null) 1258 return; 1259 1260 final _InspectorReferenceData referenceData = _idToReferenceData[id]; 1261 if (referenceData == null) 1262 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]); 1263 if (_groups[groupName]?.remove(referenceData) != true) 1264 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id is not in group')]); 1265 _decrementReferenceCount(referenceData); 1266 } 1267 1268 /// Set the list of directories that should be considered part of the local 1269 /// project. 1270 /// 1271 /// The local project directories are used to distinguish widgets created by 1272 /// the local project over widgets created from inside the framework. 1273 @protected 1274 void setPubRootDirectories(List<Object> pubRootDirectories) { 1275 _pubRootDirectories = pubRootDirectories.map<String>( 1276 (Object directory) => Uri.parse(directory).path, 1277 ).toList(); 1278 } 1279 1280 /// Set the [WidgetInspector] selection to the object matching the specified 1281 /// id if the object is valid object to set as the inspector selection. 1282 /// 1283 /// Returns true if the selection was changed. 1284 /// 1285 /// The `groupName` parameter is not required by is added to regularize the 1286 /// API surface of methods called from the Flutter IntelliJ Plugin. 1287 @protected 1288 bool setSelectionById(String id, [ String groupName ]) { 1289 return setSelection(toObject(id), groupName); 1290 } 1291 1292 /// Set the [WidgetInspector] selection to the specified `object` if it is 1293 /// a valid object to set as the inspector selection. 1294 /// 1295 /// Returns true if the selection was changed. 1296 /// 1297 /// The `groupName` parameter is not needed but is specified to regularize the 1298 /// API surface of methods called from the Flutter IntelliJ Plugin. 1299 @protected 1300 bool setSelection(Object object, [ String groupName ]) { 1301 if (object is Element || object is RenderObject) { 1302 if (object is Element) { 1303 if (object == selection.currentElement) { 1304 return false; 1305 } 1306 selection.currentElement = object; 1307 developer.inspect(selection.currentElement); 1308 } else { 1309 if (object == selection.current) { 1310 return false; 1311 } 1312 selection.current = object; 1313 developer.inspect(selection.current); 1314 } 1315 if (selectionChangedCallback != null) { 1316 if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle) { 1317 selectionChangedCallback(); 1318 } else { 1319 // It isn't safe to trigger the selection change callback if we are in 1320 // the middle of rendering the frame. 1321 SchedulerBinding.instance.scheduleTask( 1322 selectionChangedCallback, 1323 Priority.touch, 1324 ); 1325 } 1326 } 1327 return true; 1328 } 1329 return false; 1330 } 1331 1332 /// Returns JSON representing the chain of [DiagnosticsNode] instances from 1333 /// root of thee tree to the [Element] or [RenderObject] matching `id`. 1334 /// 1335 /// The JSON contains all information required to display a tree view with 1336 /// all nodes other than nodes along the path collapsed. 1337 @protected 1338 String getParentChain(String id, String groupName) { 1339 return _safeJsonEncode(_getParentChain(id, groupName)); 1340 } 1341 1342 List<Object> _getParentChain(String id, String groupName) { 1343 final Object value = toObject(id); 1344 List<_DiagnosticsPathNode> path; 1345 if (value is RenderObject) 1346 path = _getRenderObjectParentChain(value, groupName); 1347 else if (value is Element) 1348 path = _getElementParentChain(value, groupName); 1349 else 1350 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Cannot get parent chain for node of type ${value.runtimeType}')]); 1351 1352 return path.map<Object>((_DiagnosticsPathNode node) => _pathNodeToJson( 1353 node, 1354 _SerializationDelegate(groupName: groupName, service: this), 1355 )).toList(); 1356 } 1357 1358 Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, _SerializationDelegate delegate) { 1359 if (pathNode == null) 1360 return null; 1361 return <String, Object>{ 1362 'node': _nodeToJson(pathNode.node, delegate), 1363 'children': _nodesToJson(pathNode.children, delegate, parent: pathNode.node), 1364 'childIndex': pathNode.childIndex, 1365 }; 1366 } 1367 1368 List<Element> _getRawElementParentChain(Element element, { int numLocalParents }) { 1369 List<Element> elements = element?.debugGetDiagnosticChain(); 1370 if (numLocalParents != null) { 1371 for (int i = 0; i < elements.length; i += 1) { 1372 if (_isValueCreatedByLocalProject(elements[i])) { 1373 numLocalParents--; 1374 if (numLocalParents <= 0) { 1375 elements = elements.take(i + 1).toList(); 1376 break; 1377 } 1378 } 1379 } 1380 } 1381 return elements?.reversed?.toList(); 1382 } 1383 1384 List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName, { int numLocalParents }) { 1385 return _followDiagnosticableChain( 1386 _getRawElementParentChain(element, numLocalParents: numLocalParents), 1387 ) ?? const <_DiagnosticsPathNode>[]; 1388 } 1389 1390 List<_DiagnosticsPathNode> _getRenderObjectParentChain(RenderObject renderObject, String groupName, { int maxparents }) { 1391 final List<RenderObject> chain = <RenderObject>[]; 1392 while (renderObject != null) { 1393 chain.add(renderObject); 1394 renderObject = renderObject.parent; 1395 } 1396 return _followDiagnosticableChain(chain.reversed.toList()); 1397 } 1398 1399 Map<String, Object> _nodeToJson( 1400 DiagnosticsNode node, 1401 _SerializationDelegate delegate, 1402 ) { 1403 return node?.toJsonMap(delegate); 1404 } 1405 1406 bool _isValueCreatedByLocalProject(Object value) { 1407 final _Location creationLocation = _getCreationLocation(value); 1408 if (creationLocation == null) { 1409 return false; 1410 } 1411 return _isLocalCreationLocation(creationLocation); 1412 } 1413 1414 bool _isLocalCreationLocation(_Location location) { 1415 if (location == null || location.file == null) { 1416 return false; 1417 } 1418 final String file = Uri.parse(location.file).path; 1419 1420 // By default check whether the creation location was within package:flutter. 1421 if (_pubRootDirectories == null) { 1422 // TODO(chunhtai): Make it more robust once 1423 // https://github.com/flutter/flutter/issues/32660 is fixed. 1424 return !file.contains('packages/flutter/'); 1425 } 1426 for (String directory in _pubRootDirectories) { 1427 if (file.startsWith(directory)) { 1428 return true; 1429 } 1430 } 1431 return false; 1432 } 1433 1434 /// Wrapper around `json.encode` that uses a ring of cached values to prevent 1435 /// the Dart garbage collector from collecting objects between when 1436 /// the value is returned over the Observatory protocol and when the 1437 /// separate observatory protocol command has to be used to retrieve its full 1438 /// contents. 1439 // 1440 // TODO(jacobr): Replace this with a better solution once 1441 // https://github.com/dart-lang/sdk/issues/32919 is fixed. 1442 String _safeJsonEncode(Object object) { 1443 final String jsonString = json.encode(object); 1444 _serializeRing[_serializeRingIndex] = jsonString; 1445 _serializeRingIndex = (_serializeRingIndex + 1) % _serializeRing.length; 1446 return jsonString; 1447 } 1448 1449 List<DiagnosticsNode> _truncateNodes(Iterable<DiagnosticsNode> nodes, int maxDescendentsTruncatableNode) { 1450 if (nodes.every((DiagnosticsNode node) => node.value is Element) && isWidgetCreationTracked()) { 1451 final List<DiagnosticsNode> localNodes = nodes.where((DiagnosticsNode node) => 1452 _isValueCreatedByLocalProject(node.value)).toList(); 1453 if (localNodes.isNotEmpty) { 1454 return localNodes; 1455 } 1456 } 1457 return nodes.take(maxDescendentsTruncatableNode).toList(); 1458 } 1459 1460 List<Map<String, Object>> _nodesToJson( 1461 Iterable<DiagnosticsNode> nodes, 1462 _SerializationDelegate delegate, { 1463 @required DiagnosticsNode parent, 1464 }) { 1465 return DiagnosticsNode.toJsonList(nodes, parent, delegate); 1466 } 1467 1468 /// Returns a JSON representation of the properties of the [DiagnosticsNode] 1469 /// object that `diagnosticsNodeId` references. 1470 @protected 1471 String getProperties(String diagnosticsNodeId, String groupName) { 1472 return _safeJsonEncode(_getProperties(diagnosticsNodeId, groupName)); 1473 } 1474 1475 List<Object> _getProperties(String diagnosticsNodeId, String groupName) { 1476 final DiagnosticsNode node = toObject(diagnosticsNodeId); 1477 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), _SerializationDelegate(groupName: groupName, service: this), parent: node); 1478 } 1479 1480 /// Returns a JSON representation of the children of the [DiagnosticsNode] 1481 /// object that `diagnosticsNodeId` references. 1482 String getChildren(String diagnosticsNodeId, String groupName) { 1483 return _safeJsonEncode(_getChildren(diagnosticsNodeId, groupName)); 1484 } 1485 1486 List<Object> _getChildren(String diagnosticsNodeId, String groupName) { 1487 final DiagnosticsNode node = toObject(diagnosticsNodeId); 1488 final _SerializationDelegate delegate = _SerializationDelegate(groupName: groupName, service: this); 1489 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node); 1490 } 1491 1492 /// Returns a JSON representation of the children of the [DiagnosticsNode] 1493 /// object that `diagnosticsNodeId` references only including children that 1494 /// were created directly by user code. 1495 /// 1496 /// Requires [Widget] creation locations which are only available for debug 1497 /// mode builds when the `--track-widget-creation` flag is passed to 1498 /// `flutter_tool`. 1499 /// 1500 /// See also: 1501 /// 1502 /// * [isWidgetCreationTracked] which indicates whether this method can be 1503 /// used. 1504 String getChildrenSummaryTree(String diagnosticsNodeId, String groupName) { 1505 return _safeJsonEncode(_getChildrenSummaryTree(diagnosticsNodeId, groupName)); 1506 } 1507 1508 List<Object> _getChildrenSummaryTree(String diagnosticsNodeId, String groupName) { 1509 final DiagnosticsNode node = toObject(diagnosticsNodeId); 1510 final _SerializationDelegate delegate = _SerializationDelegate(groupName: groupName, summaryTree: true, service: this); 1511 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node); 1512 } 1513 1514 /// Returns a JSON representation of the children of the [DiagnosticsNode] 1515 /// object that `diagnosticsNodeId` references providing information needed 1516 /// for the details subtree view. 1517 /// 1518 /// The details subtree shows properties inline and includes all children 1519 /// rather than a filtered set of important children. 1520 String getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) { 1521 return _safeJsonEncode(_getChildrenDetailsSubtree(diagnosticsNodeId, groupName)); 1522 } 1523 1524 List<Object> _getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) { 1525 final DiagnosticsNode node = toObject(diagnosticsNodeId); 1526 // With this value of minDepth we only expand one extra level of important nodes. 1527 final _SerializationDelegate delegate = _SerializationDelegate(groupName: groupName, subtreeDepth: 1, includeProperties: true, service: this); 1528 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node); 1529 } 1530 1531 bool _shouldShowInSummaryTree(DiagnosticsNode node) { 1532 if (node.level == DiagnosticLevel.error) { 1533 return true; 1534 } 1535 final Object value = node.value; 1536 if (value is! Diagnosticable) { 1537 return true; 1538 } 1539 if (value is! Element || !isWidgetCreationTracked()) { 1540 // Creation locations are not available so include all nodes in the 1541 // summary tree. 1542 return true; 1543 } 1544 return _isValueCreatedByLocalProject(value); 1545 } 1546 1547 List<DiagnosticsNode> _getChildrenFiltered( 1548 DiagnosticsNode node, 1549 _SerializationDelegate delegate, 1550 ) { 1551 return _filterChildren(node.getChildren(), delegate); 1552 } 1553 1554 List<DiagnosticsNode> _filterChildren( 1555 List<DiagnosticsNode> nodes, 1556 _SerializationDelegate delegate, 1557 ) { 1558 final List<DiagnosticsNode> children = <DiagnosticsNode>[ 1559 for (DiagnosticsNode child in nodes) 1560 if (!delegate.summaryTree || _shouldShowInSummaryTree(child)) 1561 child 1562 else 1563 ..._getChildrenFiltered(child, delegate), 1564 ]; 1565 return children; 1566 } 1567 1568 /// Returns a JSON representation of the [DiagnosticsNode] for the root 1569 /// [Element]. 1570 String getRootWidget(String groupName) { 1571 return _safeJsonEncode(_getRootWidget(groupName)); 1572 } 1573 1574 Map<String, Object> _getRootWidget(String groupName) { 1575 return _nodeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), _SerializationDelegate(groupName: groupName, service: this)); 1576 } 1577 1578 /// Returns a JSON representation of the [DiagnosticsNode] for the root 1579 /// [Element] showing only nodes that should be included in a summary tree. 1580 String getRootWidgetSummaryTree(String groupName) { 1581 return _safeJsonEncode(_getRootWidgetSummaryTree(groupName)); 1582 } 1583 1584 Map<String, Object> _getRootWidgetSummaryTree(String groupName) { 1585 return _nodeToJson( 1586 WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), 1587 _SerializationDelegate(groupName: groupName, subtreeDepth: 1000000, summaryTree: true, service: this), 1588 ); 1589 } 1590 1591 /// Returns a JSON representation of the [DiagnosticsNode] for the root 1592 /// [RenderObject]. 1593 @protected 1594 String getRootRenderObject(String groupName) { 1595 return _safeJsonEncode(_getRootRenderObject(groupName)); 1596 } 1597 1598 Map<String, Object> _getRootRenderObject(String groupName) { 1599 return _nodeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), _SerializationDelegate(groupName: groupName, service: this)); 1600 } 1601 1602 /// Returns a JSON representation of the subtree rooted at the 1603 /// [DiagnosticsNode] object that `diagnosticsNodeId` references providing 1604 /// information needed for the details subtree view. 1605 /// 1606 /// See also: 1607 /// 1608 /// * [getChildrenDetailsSubtree], a method to get children of a node 1609 /// in the details subtree. 1610 String getDetailsSubtree(String id, String groupName) { 1611 return _safeJsonEncode(_getDetailsSubtree( id, groupName)); 1612 } 1613 1614 Map<String, Object> _getDetailsSubtree(String id, String groupName) { 1615 final DiagnosticsNode root = toObject(id); 1616 if (root == null) { 1617 return null; 1618 } 1619 return _nodeToJson( 1620 root, 1621 _SerializationDelegate( 1622 groupName: groupName, 1623 summaryTree: false, 1624 subtreeDepth: 2, // TODO(jacobr): make subtreeDepth configurable. 1625 includeProperties: true, 1626 service: this, 1627 ), 1628 ); 1629 } 1630 1631 /// Returns a [DiagnosticsNode] representing the currently selected 1632 /// [RenderObject]. 1633 /// 1634 /// If the currently selected [RenderObject] is identical to the 1635 /// [RenderObject] referenced by `previousSelectionId` then the previous 1636 /// [DiagnosticNode] is reused. 1637 @protected 1638 String getSelectedRenderObject(String previousSelectionId, String groupName) { 1639 return _safeJsonEncode(_getSelectedRenderObject(previousSelectionId, groupName)); 1640 } 1641 1642 Map<String, Object> _getSelectedRenderObject(String previousSelectionId, String groupName) { 1643 final DiagnosticsNode previousSelection = toObject(previousSelectionId); 1644 final RenderObject current = selection?.current; 1645 return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), _SerializationDelegate(groupName: groupName, service: this)); 1646 } 1647 1648 /// Returns a [DiagnosticsNode] representing the currently selected [Element]. 1649 /// 1650 /// If the currently selected [Element] is identical to the [Element] 1651 /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is 1652 /// reused. 1653 @protected 1654 String getSelectedWidget(String previousSelectionId, String groupName) { 1655 return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName)); 1656 } 1657 1658 /// Captures an image of the current state of an [object] that is a 1659 /// [RenderObject] or [Element]. 1660 /// 1661 /// The returned [ui.Image] has uncompressed raw RGBA bytes and will be scaled 1662 /// to be at most [width] pixels wide and [height] pixels tall. The returned 1663 /// image will never have a scale between logical pixels and the 1664 /// size of the output image larger than maxPixelRatio. 1665 /// [margin] indicates the number of pixels relative to the unscaled size of 1666 /// the [object] to include as a margin to include around the bounds of the 1667 /// [object] in the screenshot. Including a margin can be useful to capture 1668 /// areas that are slightly outside of the normal bounds of an object such as 1669 /// some debug paint information. 1670 @protected 1671 Future<ui.Image> screenshot( 1672 Object object, { 1673 @required double width, 1674 @required double height, 1675 double margin = 0.0, 1676 double maxPixelRatio = 1.0, 1677 bool debugPaint = false, 1678 }) async { 1679 if (object is! Element && object is! RenderObject) { 1680 return null; 1681 } 1682 final RenderObject renderObject = object is Element ? object.renderObject : object; 1683 if (renderObject == null || !renderObject.attached) { 1684 return null; 1685 } 1686 1687 if (renderObject.debugNeedsLayout) { 1688 final PipelineOwner owner = renderObject.owner; 1689 assert(owner != null); 1690 assert(!owner.debugDoingLayout); 1691 owner 1692 ..flushLayout() 1693 ..flushCompositingBits() 1694 ..flushPaint(); 1695 1696 // If we still need layout, then that means that renderObject was skipped 1697 // in the layout phase and therefore can't be painted. It is clearer to 1698 // return null indicating that a screenshot is unavailable than to return 1699 // an empty image. 1700 if (renderObject.debugNeedsLayout) { 1701 return null; 1702 } 1703 } 1704 1705 Rect renderBounds = _calculateSubtreeBounds(renderObject); 1706 if (margin != 0.0) { 1707 renderBounds = renderBounds.inflate(margin); 1708 } 1709 if (renderBounds.isEmpty) { 1710 return null; 1711 } 1712 1713 final double pixelRatio = math.min( 1714 maxPixelRatio, 1715 math.min( 1716 width / renderBounds.width, 1717 height / renderBounds.height, 1718 ), 1719 ); 1720 1721 return _ScreenshotPaintingContext.toImage( 1722 renderObject, 1723 renderBounds, 1724 pixelRatio: pixelRatio, 1725 debugPaint: debugPaint, 1726 ); 1727 } 1728 1729 Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) { 1730 final DiagnosticsNode previousSelection = toObject(previousSelectionId); 1731 final Element current = selection?.currentElement; 1732 return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), _SerializationDelegate(groupName: groupName, service: this)); 1733 } 1734 1735 /// Returns a [DiagnosticsNode] representing the currently selected [Element] 1736 /// if the selected [Element] should be shown in the summary tree otherwise 1737 /// returns the first ancestor of the selected [Element] shown in the summary 1738 /// tree. 1739 /// 1740 /// If the currently selected [Element] is identical to the [Element] 1741 /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is 1742 /// reused. 1743 String getSelectedSummaryWidget(String previousSelectionId, String groupName) { 1744 return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName)); 1745 } 1746 1747 Map<String, Object> _getSelectedSummaryWidget(String previousSelectionId, String groupName) { 1748 if (!isWidgetCreationTracked()) { 1749 return _getSelectedWidget(previousSelectionId, groupName); 1750 } 1751 final DiagnosticsNode previousSelection = toObject(previousSelectionId); 1752 Element current = selection?.currentElement; 1753 if (current != null && !_isValueCreatedByLocalProject(current)) { 1754 Element firstLocal; 1755 for (Element candidate in current.debugGetDiagnosticChain()) { 1756 if (_isValueCreatedByLocalProject(candidate)) { 1757 firstLocal = candidate; 1758 break; 1759 } 1760 } 1761 current = firstLocal; 1762 } 1763 return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), _SerializationDelegate(groupName: groupName, service: this)); 1764 } 1765 1766 /// Returns whether [Widget] creation locations are available. 1767 /// 1768 /// [Widget] creation locations are only available for debug mode builds when 1769 /// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 1770 /// is required as injecting creation locations requires a 1771 /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 1772 bool isWidgetCreationTracked() { 1773 _widgetCreationTracked ??= _WidgetForTypeTests() is _HasCreationLocation; 1774 return _widgetCreationTracked; 1775 } 1776 1777 bool _widgetCreationTracked; 1778 1779 Duration _frameStart; 1780 1781 void _onFrameStart(Duration timeStamp) { 1782 _frameStart = timeStamp; 1783 SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd); 1784 } 1785 1786 void _onFrameEnd(Duration timeStamp) { 1787 if (_trackRebuildDirtyWidgets) { 1788 _postStatsEvent('Flutter.RebuiltWidgets', _rebuildStats); 1789 } 1790 if (_trackRepaintWidgets) { 1791 _postStatsEvent('Flutter.RepaintWidgets', _repaintStats); 1792 } 1793 } 1794 1795 void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) { 1796 postEvent(eventName, stats.exportToJson(_frameStart)); 1797 } 1798 1799 /// All events dispatched by a [WidgetInspectorService] use this method 1800 /// instead of calling [developer.postEvent] directly so that tests for 1801 /// [WidgetInspectorService] can track which events were dispatched by 1802 /// overriding this method. 1803 @protected 1804 void postEvent(String eventKind, Map<Object, Object> eventData) { 1805 developer.postEvent(eventKind, eventData); 1806 } 1807 1808 final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker(); 1809 final _ElementLocationStatsTracker _repaintStats = _ElementLocationStatsTracker(); 1810 1811 void _onRebuildWidget(Element element, bool builtOnce) { 1812 _rebuildStats.add(element); 1813 } 1814 1815 void _onPaint(RenderObject renderObject) { 1816 try { 1817 final Element element = renderObject.debugCreator?.element; 1818 if (element is! RenderObjectElement) { 1819 // This branch should not hit as long as all RenderObjects were created 1820 // by Widgets. It is possible there might be some render objects 1821 // created directly without using the Widget layer so we add this check 1822 // to improve robustness. 1823 return; 1824 } 1825 _repaintStats.add(element); 1826 1827 // Give all ancestor elements credit for repainting as long as they do 1828 // not have their own associated RenderObject. 1829 element.visitAncestorElements((Element ancestor) { 1830 if (ancestor is RenderObjectElement) { 1831 // This ancestor has its own RenderObject so we can precisely track 1832 // when it repaints. 1833 return false; 1834 } 1835 _repaintStats.add(ancestor); 1836 return true; 1837 }); 1838 } 1839 catch (exception, stack) { 1840 FlutterError.reportError( 1841 FlutterErrorDetails( 1842 exception: exception, 1843 stack: stack, 1844 ), 1845 ); 1846 } 1847 } 1848 1849 /// This method is called by [WidgetBinding.performReassemble] to flush caches 1850 /// of obsolete values after a hot reload. 1851 /// 1852 /// Do not call this method directly. Instead, use 1853 /// [BindingBase.reassembleApplication]. 1854 void performReassemble() { 1855 _clearStats(); 1856 _resetErrorCount(); 1857 } 1858} 1859 1860/// Accumulator for a count associated with a specific source location. 1861/// 1862/// The accumulator stores whether the source location is [local] and what its 1863/// [id] for efficiency encoding terse JSON payloads describing counts. 1864class _LocationCount { 1865 _LocationCount({ 1866 @required this.location, 1867 @required this.id, 1868 @required this.local, 1869 }); 1870 1871 /// Location id. 1872 final int id; 1873 1874 /// Whether the location is local to the current project. 1875 final bool local; 1876 1877 final _Location location; 1878 1879 int get count => _count; 1880 int _count = 0; 1881 1882 /// Reset the count. 1883 void reset() { 1884 _count = 0; 1885 } 1886 1887 /// Increment the count. 1888 void increment() { 1889 _count++; 1890 } 1891} 1892 1893/// A stat tracker that aggregates a performance metric for [Element] objects at 1894/// the granularity of creation locations in source code. 1895/// 1896/// This class is optimized to minimize the size of the JSON payloads describing 1897/// the aggregate statistics, for stable memory usage, and low CPU usage at the 1898/// expense of somewhat higher overall memory usage. Stable memory usage is more 1899/// important than peak memory usage to avoid the false impression that the 1900/// user's app is leaking memory each frame. 1901/// 1902/// The number of unique widget creation locations tends to be at most in the 1903/// low thousands for regular flutter apps so the peak memory usage for this 1904/// class is not an issue. 1905class _ElementLocationStatsTracker { 1906 // All known creation location tracked. 1907 // 1908 // This could also be stored as a `Map<int, _LocationCount>` but this 1909 // representation is more efficient as all location ids from 0 to n are 1910 // typically present. 1911 // 1912 // All logic in this class assumes that if `_stats[i]` is not null 1913 // `_stats[i].id` equals `i`. 1914 final List<_LocationCount> _stats = <_LocationCount>[]; 1915 1916 /// Locations with a non-zero count. 1917 final List<_LocationCount> active = <_LocationCount>[]; 1918 1919 /// Locations that were added since stats were last exported. 1920 /// 1921 /// Only locations local to the current project are included as a performance 1922 /// optimization. 1923 final List<_LocationCount> newLocations = <_LocationCount>[]; 1924 1925 /// Increments the count associated with the creation location of [element] if 1926 /// the creation location is local to the current project. 1927 void add(Element element) { 1928 final Object widget = element.widget; 1929 if (widget is! _HasCreationLocation) { 1930 return; 1931 } 1932 final _HasCreationLocation creationLocationSource = widget; 1933 final _Location location = creationLocationSource._location; 1934 final int id = _toLocationId(location); 1935 1936 _LocationCount entry; 1937 if (id >= _stats.length || _stats[id] == null) { 1938 // After the first frame, almost all creation ids will already be in 1939 // _stats so this slow path will rarely be hit. 1940 while (id >= _stats.length) { 1941 _stats.add(null); 1942 } 1943 entry = _LocationCount( 1944 location: location, 1945 id: id, 1946 local: WidgetInspectorService.instance._isLocalCreationLocation(location), 1947 ); 1948 if (entry.local) { 1949 newLocations.add(entry); 1950 } 1951 _stats[id] = entry; 1952 } else { 1953 entry = _stats[id]; 1954 } 1955 1956 // We could in the future add an option to track stats for all widgets but 1957 // that would significantly increase the size of the events posted using 1958 // [developer.postEvent] and current use cases for this feature focus on 1959 // helping users find problems with their widgets not the platform 1960 // widgets. 1961 if (entry.local) { 1962 if (entry.count == 0) { 1963 active.add(entry); 1964 } 1965 entry.increment(); 1966 } 1967 } 1968 1969 /// Clear all aggregated statistics. 1970 void resetCounts() { 1971 // We chose to only reset the active counts instead of clearing all data 1972 // to reduce the number memory allocations performed after the first frame. 1973 // Once an app has warmed up, location stats tracking should not 1974 // trigger significant additional memory allocations. Avoiding memory 1975 // allocations is important to minimize the impact this class has on cpu 1976 // and memory performance of the running app. 1977 for (_LocationCount entry in active) { 1978 entry.reset(); 1979 } 1980 active.clear(); 1981 } 1982 1983 /// Exports the current counts and then resets the stats to prepare to track 1984 /// the next frame of data. 1985 Map<String, dynamic> exportToJson(Duration startTime) { 1986 final List<int> events = List<int>.filled(active.length * 2, 0); 1987 int j = 0; 1988 for (_LocationCount stat in active) { 1989 events[j++] = stat.id; 1990 events[j++] = stat.count; 1991 } 1992 1993 final Map<String, dynamic> json = <String, dynamic>{ 1994 'startTime': startTime.inMicroseconds, 1995 'events': events, 1996 }; 1997 1998 if (newLocations.isNotEmpty) { 1999 // Add all newly used location ids to the JSON. 2000 final Map<String, List<int>> locationsJson = <String, List<int>>{}; 2001 for (_LocationCount entry in newLocations) { 2002 final _Location location = entry.location; 2003 final List<int> jsonForFile = locationsJson.putIfAbsent( 2004 location.file, 2005 () => <int>[], 2006 ); 2007 jsonForFile..add(entry.id)..add(location.line)..add(location.column); 2008 } 2009 json['newLocations'] = locationsJson; 2010 } 2011 resetCounts(); 2012 newLocations.clear(); 2013 return json; 2014 } 2015} 2016 2017class _WidgetForTypeTests extends Widget { 2018 @override 2019 Element createElement() => null; 2020} 2021 2022/// A widget that enables inspecting the child widget's structure. 2023/// 2024/// Select a location on your device or emulator and view what widgets and 2025/// render object that best matches the location. An outline of the selected 2026/// widget and terse summary information is shown on device with detailed 2027/// information is shown in the observatory or in IntelliJ when using the 2028/// Flutter Plugin. 2029/// 2030/// The inspector has a select mode and a view mode. 2031/// 2032/// In the select mode, tapping the device selects the widget that best matches 2033/// the location of the touch and switches to view mode. Dragging a finger on 2034/// the device selects the widget under the drag location but does not switch 2035/// modes. Touching the very edge of the bounding box of a widget triggers 2036/// selecting the widget even if another widget that also overlaps that 2037/// location would otherwise have priority. 2038/// 2039/// In the view mode, the previously selected widget is outlined, however, 2040/// touching the device has the same effect it would have if the inspector 2041/// wasn't present. This allows interacting with the application and viewing how 2042/// the selected widget changes position. Clicking on the select icon in the 2043/// bottom left corner of the application switches back to select mode. 2044class WidgetInspector extends StatefulWidget { 2045 /// Creates a widget that enables inspection for the child. 2046 /// 2047 /// The [child] argument must not be null. 2048 const WidgetInspector({ 2049 Key key, 2050 @required this.child, 2051 @required this.selectButtonBuilder, 2052 }) : assert(child != null), 2053 super(key: key); 2054 2055 /// The widget that is being inspected. 2056 final Widget child; 2057 2058 /// A builder that is called to create the select button. 2059 /// 2060 /// The `onPressed` callback passed as an argument to the builder should be 2061 /// hooked up to the returned widget. 2062 final InspectorSelectButtonBuilder selectButtonBuilder; 2063 2064 @override 2065 _WidgetInspectorState createState() => _WidgetInspectorState(); 2066} 2067 2068class _WidgetInspectorState extends State<WidgetInspector> 2069 with WidgetsBindingObserver { 2070 2071 _WidgetInspectorState() : selection = WidgetInspectorService.instance.selection; 2072 2073 Offset _lastPointerLocation; 2074 2075 final InspectorSelection selection; 2076 2077 /// Whether the inspector is in select mode. 2078 /// 2079 /// In select mode, pointer interactions trigger widget selection instead of 2080 /// normal interactions. Otherwise the previously selected widget is 2081 /// highlighted but the application can be interacted with normally. 2082 bool isSelectMode = true; 2083 2084 final GlobalKey _ignorePointerKey = GlobalKey(); 2085 2086 /// Distance from the edge of of the bounding box for an element to consider 2087 /// as selecting the edge of the bounding box. 2088 static const double _edgeHitMargin = 2.0; 2089 2090 InspectorSelectionChangedCallback _selectionChangedCallback; 2091 @override 2092 void initState() { 2093 super.initState(); 2094 2095 _selectionChangedCallback = () { 2096 setState(() { 2097 // The [selection] property which the build method depends on has 2098 // changed. 2099 }); 2100 }; 2101 WidgetInspectorService.instance.selectionChangedCallback = _selectionChangedCallback; 2102 } 2103 2104 @override 2105 void dispose() { 2106 if (WidgetInspectorService.instance.selectionChangedCallback == _selectionChangedCallback) { 2107 WidgetInspectorService.instance.selectionChangedCallback = null; 2108 } 2109 super.dispose(); 2110 } 2111 2112 bool _hitTestHelper( 2113 List<RenderObject> hits, 2114 List<RenderObject> edgeHits, 2115 Offset position, 2116 RenderObject object, 2117 Matrix4 transform, 2118 ) { 2119 bool hit = false; 2120 final Matrix4 inverse = Matrix4.tryInvert(transform); 2121 if (inverse == null) { 2122 // We cannot invert the transform. That means the object doesn't appear on 2123 // screen and cannot be hit. 2124 return false; 2125 } 2126 final Offset localPosition = MatrixUtils.transformPoint(inverse, position); 2127 2128 final List<DiagnosticsNode> children = object.debugDescribeChildren(); 2129 for (int i = children.length - 1; i >= 0; i -= 1) { 2130 final DiagnosticsNode diagnostics = children[i]; 2131 assert(diagnostics != null); 2132 if (diagnostics.style == DiagnosticsTreeStyle.offstage || 2133 diagnostics.value is! RenderObject) 2134 continue; 2135 final RenderObject child = diagnostics.value; 2136 final Rect paintClip = object.describeApproximatePaintClip(child); 2137 if (paintClip != null && !paintClip.contains(localPosition)) 2138 continue; 2139 2140 final Matrix4 childTransform = transform.clone(); 2141 object.applyPaintTransform(child, childTransform); 2142 if (_hitTestHelper(hits, edgeHits, position, child, childTransform)) 2143 hit = true; 2144 } 2145 2146 final Rect bounds = object.semanticBounds; 2147 if (bounds.contains(localPosition)) { 2148 hit = true; 2149 // Hits that occur on the edge of the bounding box of an object are 2150 // given priority to provide a way to select objects that would 2151 // otherwise be hard to select. 2152 if (!bounds.deflate(_edgeHitMargin).contains(localPosition)) 2153 edgeHits.add(object); 2154 } 2155 if (hit) 2156 hits.add(object); 2157 return hit; 2158 } 2159 2160 /// Returns the list of render objects located at the given position ordered 2161 /// by priority. 2162 /// 2163 /// All render objects that are not offstage that match the location are 2164 /// included in the list of matches. Priority is given to matches that occur 2165 /// on the edge of a render object's bounding box and to matches found by 2166 /// [RenderBox.hitTest]. 2167 List<RenderObject> hitTest(Offset position, RenderObject root) { 2168 final List<RenderObject> regularHits = <RenderObject>[]; 2169 final List<RenderObject> edgeHits = <RenderObject>[]; 2170 2171 _hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null)); 2172 // Order matches by the size of the hit area. 2173 double _area(RenderObject object) { 2174 final Size size = object.semanticBounds?.size; 2175 return size == null ? double.maxFinite : size.width * size.height; 2176 } 2177 regularHits.sort((RenderObject a, RenderObject b) => _area(a).compareTo(_area(b))); 2178 final Set<RenderObject> hits = <RenderObject>{ 2179 ...edgeHits, 2180 ...regularHits, 2181 }; 2182 return hits.toList(); 2183 } 2184 2185 void _inspectAt(Offset position) { 2186 if (!isSelectMode) 2187 return; 2188 2189 final RenderIgnorePointer ignorePointer = _ignorePointerKey.currentContext.findRenderObject(); 2190 final RenderObject userRender = ignorePointer.child; 2191 final List<RenderObject> selected = hitTest(position, userRender); 2192 2193 setState(() { 2194 selection.candidates = selected; 2195 }); 2196 } 2197 2198 void _handlePanDown(DragDownDetails event) { 2199 _lastPointerLocation = event.globalPosition; 2200 _inspectAt(event.globalPosition); 2201 } 2202 2203 void _handlePanUpdate(DragUpdateDetails event) { 2204 _lastPointerLocation = event.globalPosition; 2205 _inspectAt(event.globalPosition); 2206 } 2207 2208 void _handlePanEnd(DragEndDetails details) { 2209 // If the pan ends on the edge of the window assume that it indicates the 2210 // pointer is being dragged off the edge of the display not a regular touch 2211 // on the edge of the display. If the pointer is being dragged off the edge 2212 // of the display we do not want to select anything. A user can still select 2213 // a widget that is only at the exact screen margin by tapping. 2214 final Rect bounds = (Offset.zero & (WidgetsBinding.instance.window.physicalSize / WidgetsBinding.instance.window.devicePixelRatio)).deflate(_kOffScreenMargin); 2215 if (!bounds.contains(_lastPointerLocation)) { 2216 setState(() { 2217 selection.clear(); 2218 }); 2219 } 2220 } 2221 2222 void _handleTap() { 2223 if (!isSelectMode) 2224 return; 2225 if (_lastPointerLocation != null) { 2226 _inspectAt(_lastPointerLocation); 2227 2228 if (selection != null) { 2229 // Notify debuggers to open an inspector on the object. 2230 developer.inspect(selection.current); 2231 } 2232 } 2233 setState(() { 2234 // Only exit select mode if there is a button to return to select mode. 2235 if (widget.selectButtonBuilder != null) 2236 isSelectMode = false; 2237 }); 2238 } 2239 2240 void _handleEnableSelect() { 2241 setState(() { 2242 isSelectMode = true; 2243 }); 2244 } 2245 2246 @override 2247 Widget build(BuildContext context) { 2248 final List<Widget> children = <Widget>[]; 2249 children.add(GestureDetector( 2250 onTap: _handleTap, 2251 onPanDown: _handlePanDown, 2252 onPanEnd: _handlePanEnd, 2253 onPanUpdate: _handlePanUpdate, 2254 behavior: HitTestBehavior.opaque, 2255 excludeFromSemantics: true, 2256 child: IgnorePointer( 2257 ignoring: isSelectMode, 2258 key: _ignorePointerKey, 2259 ignoringSemantics: false, 2260 child: widget.child, 2261 ), 2262 )); 2263 if (!isSelectMode && widget.selectButtonBuilder != null) { 2264 children.add(Positioned( 2265 left: _kInspectButtonMargin, 2266 bottom: _kInspectButtonMargin, 2267 child: widget.selectButtonBuilder(context, _handleEnableSelect), 2268 )); 2269 } 2270 children.add(_InspectorOverlay(selection: selection)); 2271 return Stack(children: children); 2272 } 2273} 2274 2275/// Mutable selection state of the inspector. 2276class InspectorSelection { 2277 /// Render objects that are candidates to be selected. 2278 /// 2279 /// Tools may wish to iterate through the list of candidates. 2280 List<RenderObject> get candidates => _candidates; 2281 List<RenderObject> _candidates = <RenderObject>[]; 2282 set candidates(List<RenderObject> value) { 2283 _candidates = value; 2284 _index = 0; 2285 _computeCurrent(); 2286 } 2287 2288 /// Index within the list of candidates that is currently selected. 2289 int get index => _index; 2290 int _index = 0; 2291 set index(int value) { 2292 _index = value; 2293 _computeCurrent(); 2294 } 2295 2296 /// Set the selection to empty. 2297 void clear() { 2298 _candidates = <RenderObject>[]; 2299 _index = 0; 2300 _computeCurrent(); 2301 } 2302 2303 /// Selected render object typically from the [candidates] list. 2304 /// 2305 /// Setting [candidates] or calling [clear] resets the selection. 2306 /// 2307 /// Returns null if the selection is invalid. 2308 RenderObject get current => _current; 2309 RenderObject _current; 2310 set current(RenderObject value) { 2311 if (_current != value) { 2312 _current = value; 2313 _currentElement = value.debugCreator.element; 2314 } 2315 } 2316 2317 /// Selected [Element] consistent with the [current] selected [RenderObject]. 2318 /// 2319 /// Setting [candidates] or calling [clear] resets the selection. 2320 /// 2321 /// Returns null if the selection is invalid. 2322 Element get currentElement => _currentElement; 2323 Element _currentElement; 2324 set currentElement(Element element) { 2325 if (currentElement != element) { 2326 _currentElement = element; 2327 _current = element.findRenderObject(); 2328 } 2329 } 2330 2331 void _computeCurrent() { 2332 if (_index < candidates.length) { 2333 _current = candidates[index]; 2334 _currentElement = _current.debugCreator.element; 2335 } else { 2336 _current = null; 2337 _currentElement = null; 2338 } 2339 } 2340 2341 /// Whether the selected render object is attached to the tree or has gone 2342 /// out of scope. 2343 bool get active => _current != null && _current.attached; 2344} 2345 2346class _InspectorOverlay extends LeafRenderObjectWidget { 2347 const _InspectorOverlay({ 2348 Key key, 2349 @required this.selection, 2350 }) : super(key: key); 2351 2352 final InspectorSelection selection; 2353 2354 @override 2355 _RenderInspectorOverlay createRenderObject(BuildContext context) { 2356 return _RenderInspectorOverlay(selection: selection); 2357 } 2358 2359 @override 2360 void updateRenderObject(BuildContext context, _RenderInspectorOverlay renderObject) { 2361 renderObject.selection = selection; 2362 } 2363} 2364 2365class _RenderInspectorOverlay extends RenderBox { 2366 /// The arguments must not be null. 2367 _RenderInspectorOverlay({ @required InspectorSelection selection }) 2368 : _selection = selection, 2369 assert(selection != null); 2370 2371 InspectorSelection get selection => _selection; 2372 InspectorSelection _selection; 2373 set selection(InspectorSelection value) { 2374 if (value != _selection) { 2375 _selection = value; 2376 } 2377 markNeedsPaint(); 2378 } 2379 2380 @override 2381 bool get sizedByParent => true; 2382 2383 @override 2384 bool get alwaysNeedsCompositing => true; 2385 2386 @override 2387 void performResize() { 2388 size = constraints.constrain(const Size(double.infinity, double.infinity)); 2389 } 2390 2391 @override 2392 void paint(PaintingContext context, Offset offset) { 2393 assert(needsCompositing); 2394 context.addLayer(_InspectorOverlayLayer( 2395 overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height), 2396 selection: selection, 2397 )); 2398 } 2399} 2400 2401class _TransformedRect { 2402 _TransformedRect(RenderObject object) 2403 : rect = object.semanticBounds, 2404 transform = object.getTransformTo(null); 2405 2406 final Rect rect; 2407 final Matrix4 transform; 2408 2409 @override 2410 bool operator ==(dynamic other) { 2411 if (other.runtimeType != runtimeType) 2412 return false; 2413 final _TransformedRect typedOther = other; 2414 return rect == typedOther.rect && transform == typedOther.transform; 2415 } 2416 2417 @override 2418 int get hashCode => hashValues(rect, transform); 2419} 2420 2421/// State describing how the inspector overlay should be rendered. 2422/// 2423/// The equality operator can be used to determine whether the overlay needs to 2424/// be rendered again. 2425class _InspectorOverlayRenderState { 2426 _InspectorOverlayRenderState({ 2427 @required this.overlayRect, 2428 @required this.selected, 2429 @required this.candidates, 2430 @required this.tooltip, 2431 @required this.textDirection, 2432 }); 2433 2434 final Rect overlayRect; 2435 final _TransformedRect selected; 2436 final List<_TransformedRect> candidates; 2437 final String tooltip; 2438 final TextDirection textDirection; 2439 2440 @override 2441 bool operator ==(dynamic other) { 2442 if (other.runtimeType != runtimeType) 2443 return false; 2444 2445 final _InspectorOverlayRenderState typedOther = other; 2446 return overlayRect == typedOther.overlayRect 2447 && selected == typedOther.selected 2448 && listEquals<_TransformedRect>(candidates, typedOther.candidates) 2449 && tooltip == typedOther.tooltip; 2450 } 2451 2452 @override 2453 int get hashCode => hashValues(overlayRect, selected, hashList(candidates), tooltip); 2454} 2455 2456const int _kMaxTooltipLines = 5; 2457const Color _kTooltipBackgroundColor = Color.fromARGB(230, 60, 60, 60); 2458const Color _kHighlightedRenderObjectFillColor = Color.fromARGB(128, 128, 128, 255); 2459const Color _kHighlightedRenderObjectBorderColor = Color.fromARGB(128, 64, 64, 128); 2460 2461/// A layer that outlines the selected [RenderObject] and candidate render 2462/// objects that also match the last pointer location. 2463/// 2464/// This approach is horrific for performance and is only used here because this 2465/// is limited to debug mode. Do not duplicate the logic in production code. 2466class _InspectorOverlayLayer extends Layer { 2467 /// Creates a layer that displays the inspector overlay. 2468 _InspectorOverlayLayer({ 2469 @required this.overlayRect, 2470 @required this.selection, 2471 }) : assert(overlayRect != null), 2472 assert(selection != null) { 2473 bool inDebugMode = false; 2474 assert(() { 2475 inDebugMode = true; 2476 return true; 2477 }()); 2478 if (inDebugMode == false) { 2479 throw FlutterError.fromParts(<DiagnosticsNode>[ 2480 ErrorSummary( 2481 'The inspector should never be used in production mode due to the ' 2482 'negative performance impact.' 2483 ) 2484 ]); 2485 } 2486 } 2487 2488 InspectorSelection selection; 2489 2490 /// The rectangle in this layer's coordinate system that the overlay should 2491 /// occupy. 2492 /// 2493 /// The scene must be explicitly recomposited after this property is changed 2494 /// (as described at [Layer]). 2495 final Rect overlayRect; 2496 2497 _InspectorOverlayRenderState _lastState; 2498 2499 /// Picture generated from _lastState. 2500 ui.Picture _picture; 2501 2502 TextPainter _textPainter; 2503 double _textPainterMaxWidth; 2504 2505 @override 2506 void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) { 2507 if (!selection.active) 2508 return; 2509 2510 final RenderObject selected = selection.current; 2511 final List<_TransformedRect> candidates = <_TransformedRect>[]; 2512 for (RenderObject candidate in selection.candidates) { 2513 if (candidate == selected || !candidate.attached) 2514 continue; 2515 candidates.add(_TransformedRect(candidate)); 2516 } 2517 2518 final _InspectorOverlayRenderState state = _InspectorOverlayRenderState( 2519 overlayRect: overlayRect, 2520 selected: _TransformedRect(selected), 2521 tooltip: selection.currentElement.toStringShort(), 2522 textDirection: TextDirection.ltr, 2523 candidates: candidates, 2524 ); 2525 2526 if (state != _lastState) { 2527 _lastState = state; 2528 _picture = _buildPicture(state); 2529 } 2530 builder.addPicture(layerOffset, _picture); 2531 } 2532 2533 ui.Picture _buildPicture(_InspectorOverlayRenderState state) { 2534 final ui.PictureRecorder recorder = ui.PictureRecorder(); 2535 final Canvas canvas = Canvas(recorder, state.overlayRect); 2536 final Size size = state.overlayRect.size; 2537 2538 final Paint fillPaint = Paint() 2539 ..style = PaintingStyle.fill 2540 ..color = _kHighlightedRenderObjectFillColor; 2541 2542 final Paint borderPaint = Paint() 2543 ..style = PaintingStyle.stroke 2544 ..strokeWidth = 1.0 2545 ..color = _kHighlightedRenderObjectBorderColor; 2546 2547 // Highlight the selected renderObject. 2548 final Rect selectedPaintRect = state.selected.rect.deflate(0.5); 2549 canvas 2550 ..save() 2551 ..transform(state.selected.transform.storage) 2552 ..drawRect(selectedPaintRect, fillPaint) 2553 ..drawRect(selectedPaintRect, borderPaint) 2554 ..restore(); 2555 2556 // Show all other candidate possibly selected elements. This helps selecting 2557 // render objects by selecting the edge of the bounding box shows all 2558 // elements the user could toggle the selection between. 2559 for (_TransformedRect transformedRect in state.candidates) { 2560 canvas 2561 ..save() 2562 ..transform(transformedRect.transform.storage) 2563 ..drawRect(transformedRect.rect.deflate(0.5), borderPaint) 2564 ..restore(); 2565 } 2566 2567 final Rect targetRect = MatrixUtils.transformRect( 2568 state.selected.transform, state.selected.rect); 2569 final Offset target = Offset(targetRect.left, targetRect.center.dy); 2570 const double offsetFromWidget = 9.0; 2571 final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; 2572 2573 _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect); 2574 2575 // TODO(jacobr): provide an option to perform a debug paint of just the 2576 // selected widget. 2577 return recorder.endRecording(); 2578 } 2579 2580 void _paintDescription( 2581 Canvas canvas, 2582 String message, 2583 TextDirection textDirection, 2584 Offset target, 2585 double verticalOffset, 2586 Size size, 2587 Rect targetRect, 2588 ) { 2589 canvas.save(); 2590 final double maxWidth = size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding); 2591 final TextSpan textSpan = _textPainter?.text; 2592 if (_textPainter == null || textSpan.text != message || _textPainterMaxWidth != maxWidth) { 2593 _textPainterMaxWidth = maxWidth; 2594 _textPainter = TextPainter() 2595 ..maxLines = _kMaxTooltipLines 2596 ..ellipsis = '...' 2597 ..text = TextSpan(style: _messageStyle, text: message) 2598 ..textDirection = textDirection 2599 ..layout(maxWidth: maxWidth); 2600 } 2601 2602 final Size tooltipSize = _textPainter.size + const Offset(_kTooltipPadding * 2, _kTooltipPadding * 2); 2603 final Offset tipOffset = positionDependentBox( 2604 size: size, 2605 childSize: tooltipSize, 2606 target: target, 2607 verticalOffset: verticalOffset, 2608 preferBelow: false, 2609 ); 2610 2611 final Paint tooltipBackground = Paint() 2612 ..style = PaintingStyle.fill 2613 ..color = _kTooltipBackgroundColor; 2614 canvas.drawRect( 2615 Rect.fromPoints( 2616 tipOffset, 2617 tipOffset.translate(tooltipSize.width, tooltipSize.height), 2618 ), 2619 tooltipBackground, 2620 ); 2621 2622 double wedgeY = tipOffset.dy; 2623 final bool tooltipBelow = tipOffset.dy > target.dy; 2624 if (!tooltipBelow) 2625 wedgeY += tooltipSize.height; 2626 2627 const double wedgeSize = _kTooltipPadding * 2; 2628 double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2; 2629 wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2); 2630 final List<Offset> wedge = <Offset>[ 2631 Offset(wedgeX - wedgeSize, wedgeY), 2632 Offset(wedgeX + wedgeSize, wedgeY), 2633 Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)), 2634 ]; 2635 canvas.drawPath(Path()..addPolygon(wedge, true,), tooltipBackground); 2636 _textPainter.paint(canvas, tipOffset + const Offset(_kTooltipPadding, _kTooltipPadding)); 2637 canvas.restore(); 2638 } 2639 2640 @override 2641 S find<S>(Offset regionOffset) => null; 2642 2643 @override 2644 Iterable<S> findAll<S>(Offset regionOffset) => <S>[]; 2645} 2646 2647const double _kScreenEdgeMargin = 10.0; 2648const double _kTooltipPadding = 5.0; 2649const double _kInspectButtonMargin = 10.0; 2650 2651/// Interpret pointer up events within with this margin as indicating the 2652/// pointer is moving off the device. 2653const double _kOffScreenMargin = 1.0; 2654 2655const TextStyle _messageStyle = TextStyle( 2656 color: Color(0xFFFFFFFF), 2657 fontSize: 10.0, 2658 height: 1.2, 2659); 2660 2661/// Interface for classes that track the source code location the their 2662/// constructor was called from. 2663/// 2664/// A [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 2665/// adds this interface to the [Widget] class when the 2666/// `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is 2667/// required as injecting creation locations requires a 2668/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 2669// ignore: unused_element 2670abstract class _HasCreationLocation { 2671 _Location get _location; 2672} 2673 2674/// A tuple with file, line, and column number, for displaying human-readable 2675/// file locations. 2676class _Location { 2677 const _Location({ 2678 this.file, 2679 this.line, 2680 this.column, 2681 this.name, 2682 this.parameterLocations, 2683 }); 2684 2685 /// File path of the location. 2686 final String file; 2687 2688 /// 1-based line number. 2689 final int line; 2690 /// 1-based column number. 2691 final int column; 2692 2693 /// Optional name of the parameter or function at this location. 2694 final String name; 2695 2696 /// Optional locations of the parameters of the member at this location. 2697 final List<_Location> parameterLocations; 2698 2699 Map<String, Object> toJsonMap() { 2700 final Map<String, Object> json = <String, Object>{ 2701 'file': file, 2702 'line': line, 2703 'column': column, 2704 }; 2705 if (name != null) { 2706 json['name'] = name; 2707 } 2708 if (parameterLocations != null) { 2709 json['parameterLocations'] = parameterLocations.map<Map<String, Object>>( 2710 (_Location location) => location.toJsonMap()).toList(); 2711 } 2712 return json; 2713 } 2714 2715 @override 2716 String toString() { 2717 final List<String> parts = <String>[]; 2718 if (name != null) { 2719 parts.add(name); 2720 } 2721 if (file != null) { 2722 parts.add(file); 2723 } 2724 parts..add('$line')..add('$column'); 2725 return parts.join(':'); 2726 } 2727} 2728 2729bool _isDebugCreator(DiagnosticsNode node) => node is DiagnosticsDebugCreator; 2730 2731/// Transformer to parse and gather information about [DiagnosticsDebugCreator]. 2732/// 2733/// This function will be registered to [FlutterErrorDetails.propertiesTransformers] 2734/// in [WidgetsBinding.initInstances]. 2735Iterable<DiagnosticsNode> transformDebugCreator(Iterable<DiagnosticsNode> properties) sync* { 2736 final List<DiagnosticsNode> pending = <DiagnosticsNode>[]; 2737 bool foundStackTrace = false; 2738 for (DiagnosticsNode node in properties) { 2739 if (!foundStackTrace && node is DiagnosticsStackTrace) 2740 foundStackTrace = true; 2741 if (_isDebugCreator(node)) { 2742 yield* _parseDiagnosticsNode(node); 2743 } else { 2744 if (foundStackTrace) { 2745 pending.add(node); 2746 } else { 2747 yield node; 2748 } 2749 } 2750 } 2751 yield* pending; 2752} 2753 2754/// Transform the input [DiagnosticsNode]. 2755/// 2756/// Return null if input [DiagnosticsNode] is not applicable. 2757Iterable<DiagnosticsNode> _parseDiagnosticsNode(DiagnosticsNode node) { 2758 if (!_isDebugCreator(node)) 2759 return null; 2760 final DebugCreator debugCreator = node.value; 2761 final Element element = debugCreator.element; 2762 return _describeRelevantUserCode(element); 2763} 2764 2765Iterable<DiagnosticsNode> _describeRelevantUserCode(Element element) { 2766 if (!WidgetInspectorService.instance.isWidgetCreationTracked()) { 2767 return <DiagnosticsNode>[ 2768 ErrorDescription( 2769 'Widget creation tracking is currently disabled. Enabling ' 2770 'it enables improved error messages. It can be enabled by passing ' 2771 '`--track-widget-creation` to `flutter run` or `flutter test`.', 2772 ), 2773 ErrorSpacer(), 2774 ]; 2775 } 2776 final List<DiagnosticsNode> nodes = <DiagnosticsNode>[]; 2777 element.visitAncestorElements((Element ancestor) { 2778 // TODO(chunhtai): should print out all the widgets that are about to cross 2779 // package boundaries. 2780 if (_isLocalCreationLocation(ancestor)) { 2781 nodes.add( 2782 DiagnosticsBlock( 2783 name: 'User-created ancestor of the error-causing widget was', 2784 children: <DiagnosticsNode>[ 2785 ErrorDescription('${ancestor.widget.toStringShort()} ${_describeCreationLocation(ancestor)}'), 2786 ], 2787 ) 2788 ); 2789 nodes.add(ErrorSpacer()); 2790 return false; 2791 } 2792 return true; 2793 }); 2794 return nodes; 2795} 2796 2797/// Returns if an object is user created. 2798/// 2799/// This function will only work in debug mode builds when 2800/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is 2801/// required as injecting creation locations requires a 2802/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 2803/// 2804/// Currently is local creation locations are only available for 2805/// [Widget] and [Element]. 2806bool _isLocalCreationLocation(Object object) { 2807 final _Location location = _getCreationLocation(object); 2808 if (location == null) 2809 return false; 2810 return WidgetInspectorService.instance._isLocalCreationLocation(location); 2811} 2812 2813/// Returns the creation location of an object in String format if one is available. 2814/// 2815/// ex: "file:///path/to/main.dart:4:3" 2816/// 2817/// Creation locations are only available for debug mode builds when 2818/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is 2819/// required as injecting creation locations requires a 2820/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 2821/// 2822/// Currently creation locations are only available for [Widget] and [Element]. 2823String _describeCreationLocation(Object object) { 2824 final _Location location = _getCreationLocation(object); 2825 return location?.toString(); 2826} 2827 2828/// Returns the creation location of an object if one is available. 2829/// 2830/// Creation locations are only available for debug mode builds when 2831/// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is 2832/// required as injecting creation locations requires a 2833/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). 2834/// 2835/// Currently creation locations are only available for [Widget] and [Element]. 2836_Location _getCreationLocation(Object object) { 2837 final Object candidate = object is Element ? object.widget : object; 2838 return candidate is _HasCreationLocation ? candidate._location : null; 2839} 2840 2841// _Location objects are always const so we don't need to worry about the GC 2842// issues that are a concern for other object ids tracked by 2843// [WidgetInspectorService]. 2844final Map<_Location, int> _locationToId = <_Location, int>{}; 2845final List<_Location> _locations = <_Location>[]; 2846 2847int _toLocationId(_Location location) { 2848 int id = _locationToId[location]; 2849 if (id != null) { 2850 return id; 2851 } 2852 id = _locations.length; 2853 _locations.add(location); 2854 _locationToId[location] = id; 2855 return id; 2856} 2857 2858class _SerializationDelegate implements DiagnosticsSerializationDelegate { 2859 _SerializationDelegate({ 2860 this.groupName, 2861 this.summaryTree = false, 2862 this.maxDescendentsTruncatableNode = -1, 2863 this.expandPropertyValues = true, 2864 this.subtreeDepth = 1, 2865 this.includeProperties = false, 2866 @required this.service, 2867 }); 2868 2869 final WidgetInspectorService service; 2870 final String groupName; 2871 final bool summaryTree; 2872 final int maxDescendentsTruncatableNode; 2873 2874 @override 2875 final bool includeProperties; 2876 2877 @override 2878 final int subtreeDepth; 2879 2880 @override 2881 final bool expandPropertyValues; 2882 2883 final List<DiagnosticsNode> _nodesCreatedByLocalProject = <DiagnosticsNode>[]; 2884 2885 bool get interactive => groupName != null; 2886 2887 @override 2888 Map<String, Object> additionalNodeProperties(DiagnosticsNode node) { 2889 final Map<String, Object> result = <String, Object>{}; 2890 final Object value = node.value; 2891 if (interactive) { 2892 result['objectId'] = service.toId(node, groupName); 2893 result['valueId'] = service.toId(value, groupName); 2894 } 2895 if (summaryTree) { 2896 result['summaryTree'] = true; 2897 } 2898 final _Location creationLocation = _getCreationLocation(value); 2899 if (creationLocation != null) { 2900 result['locationId'] = _toLocationId(creationLocation); 2901 result['creationLocation'] = creationLocation.toJsonMap(); 2902 if (service._isLocalCreationLocation(creationLocation)) { 2903 _nodesCreatedByLocalProject.add(node); 2904 result['createdByLocalProject'] = true; 2905 } 2906 } 2907 return result; 2908 } 2909 2910 @override 2911 DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) { 2912 // The tricky special case here is that when in the detailsTree, 2913 // we keep subtreeDepth from going down to zero until we reach nodes 2914 // that also exist in the summary tree. This ensures that every time 2915 // you expand a node in the details tree, you expand the entire subtree 2916 // up until you reach the next nodes shared with the summary tree. 2917 return summaryTree || subtreeDepth > 1 || service._shouldShowInSummaryTree(node) 2918 ? copyWith(subtreeDepth: subtreeDepth - 1) 2919 : this; 2920 } 2921 2922 @override 2923 List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> children, DiagnosticsNode owner) { 2924 return service._filterChildren(children, this); 2925 } 2926 2927 @override 2928 List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> properties, DiagnosticsNode owner) { 2929 final bool createdByLocalProject = _nodesCreatedByLocalProject.contains(owner); 2930 return properties.where((DiagnosticsNode node) { 2931 return !node.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info); 2932 }).toList(); 2933 } 2934 2935 @override 2936 List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode owner) { 2937 if (maxDescendentsTruncatableNode >= 0 && 2938 owner?.allowTruncate == true && 2939 nodes.length > maxDescendentsTruncatableNode) { 2940 nodes = service._truncateNodes(nodes, maxDescendentsTruncatableNode); 2941 } 2942 return nodes; 2943 } 2944 2945 @override 2946 DiagnosticsSerializationDelegate copyWith({int subtreeDepth, bool includeProperties}) { 2947 return _SerializationDelegate( 2948 groupName: groupName, 2949 summaryTree: summaryTree, 2950 maxDescendentsTruncatableNode: maxDescendentsTruncatableNode, 2951 expandPropertyValues: expandPropertyValues, 2952 subtreeDepth: subtreeDepth ?? this.subtreeDepth, 2953 includeProperties: includeProperties ?? this.includeProperties, 2954 service: service, 2955 ); 2956 } 2957} 2958