1// Copyright 2014 The Flutter 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:collection' show HashMap;
7import 'dart:convert';
8import 'dart:developer' as developer;
9import 'dart:math' as math;
10import 'dart:ui' as ui
11 show
12 ClipOp,
13 FlutterView,
14 Image,
15 ImageByteFormat,
16 Paragraph,
17 Picture,
18 PictureRecorder,
19 PointMode,
20 SceneBuilder,
21 Vertices;
22
23import 'package:flutter/foundation.dart';
24import 'package:flutter/rendering.dart';
25import 'package:flutter/scheduler.dart';
26import 'package:meta/meta_meta.dart';
27
28import 'app.dart';
29import 'basic.dart';
30import 'binding.dart';
31import 'debug.dart';
32import 'framework.dart';
33import 'gesture_detector.dart';
34import 'service_extensions.dart';
35import 'view.dart';
36
37/// Signature for the builder callback used by
38/// [WidgetInspector.selectButtonBuilder].
39typedef InspectorSelectButtonBuilder = Widget Function(BuildContext context, VoidCallback onPressed);
40
41/// Signature for a method that registers the service extension `callback` with
42/// the given `name`.
43///
44/// Used as argument to [WidgetInspectorService.initServiceExtensions]. The
45/// [BindingBase.registerServiceExtension] implements this signature.
46typedef RegisterServiceExtensionCallback = void Function({
47 required String name,
48 required ServiceExtensionCallback callback,
49});
50
51/// A layer that mimics the behavior of another layer.
52///
53/// A proxy layer is used for cases where a layer needs to be placed into
54/// multiple trees of layers.
55class _ProxyLayer extends Layer {
56 _ProxyLayer(this._layer);
57
58 final Layer _layer;
59
60 @override
61 void addToScene(ui.SceneBuilder builder) {
62 _layer.addToScene(builder);
63 }
64
65 @override
66 @protected
67 bool findAnnotations<S extends Object>(
68 AnnotationResult<S> result,
69 Offset localPosition, {
70 required bool onlyFirst,
71 }) {
72 return _layer.findAnnotations(result, localPosition, onlyFirst: onlyFirst);
73 }
74}
75
76/// A [Canvas] that multicasts all method calls to a main canvas and a
77/// secondary screenshot canvas so that a screenshot can be recorded at the same
78/// time as performing a normal paint.
79class _MulticastCanvas implements Canvas {
80 _MulticastCanvas({
81 required Canvas main,
82 required Canvas screenshot,
83 }) : _main = main,
84 _screenshot = screenshot;
85
86 final Canvas _main;
87 final Canvas _screenshot;
88
89 @override
90 void clipPath(Path path, { bool doAntiAlias = true }) {
91 _main.clipPath(path, doAntiAlias: doAntiAlias);
92 _screenshot.clipPath(path, doAntiAlias: doAntiAlias);
93 }
94
95 @override
96 void clipRRect(RRect rrect, { bool doAntiAlias = true }) {
97 _main.clipRRect(rrect, doAntiAlias: doAntiAlias);
98 _screenshot.clipRRect(rrect, doAntiAlias: doAntiAlias);
99 }
100
101 @override
102 void clipRect(Rect rect, { ui.ClipOp clipOp = ui.ClipOp.intersect, bool doAntiAlias = true }) {
103 _main.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
104 _screenshot.clipRect(rect, clipOp: clipOp, doAntiAlias: doAntiAlias);
105 }
106
107 @override
108 void drawArc(Rect rect, double startAngle, double sweepAngle, bool useCenter, Paint paint) {
109 _main.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
110 _screenshot.drawArc(rect, startAngle, sweepAngle, useCenter, paint);
111 }
112
113 @override
114 void drawAtlas(ui.Image atlas, List<RSTransform> transforms, List<Rect> rects, List<Color>? colors, BlendMode? blendMode, Rect? cullRect, Paint paint) {
115 _main.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
116 _screenshot.drawAtlas(atlas, transforms, rects, colors, blendMode, cullRect, paint);
117 }
118
119 @override
120 void drawCircle(Offset c, double radius, Paint paint) {
121 _main.drawCircle(c, radius, paint);
122 _screenshot.drawCircle(c, radius, paint);
123 }
124
125 @override
126 void drawColor(Color color, BlendMode blendMode) {
127 _main.drawColor(color, blendMode);
128 _screenshot.drawColor(color, blendMode);
129 }
130
131 @override
132 void drawDRRect(RRect outer, RRect inner, Paint paint) {
133 _main.drawDRRect(outer, inner, paint);
134 _screenshot.drawDRRect(outer, inner, paint);
135 }
136
137 @override
138 void drawImage(ui.Image image, Offset p, Paint paint) {
139 _main.drawImage(image, p, paint);
140 _screenshot.drawImage(image, p, paint);
141 }
142
143 @override
144 void drawImageNine(ui.Image image, Rect center, Rect dst, Paint paint) {
145 _main.drawImageNine(image, center, dst, paint);
146 _screenshot.drawImageNine(image, center, dst, paint);
147 }
148
149 @override
150 void drawImageRect(ui.Image image, Rect src, Rect dst, Paint paint) {
151 _main.drawImageRect(image, src, dst, paint);
152 _screenshot.drawImageRect(image, src, dst, paint);
153 }
154
155 @override
156 void drawLine(Offset p1, Offset p2, Paint paint) {
157 _main.drawLine(p1, p2, paint);
158 _screenshot.drawLine(p1, p2, paint);
159 }
160
161 @override
162 void drawOval(Rect rect, Paint paint) {
163 _main.drawOval(rect, paint);
164 _screenshot.drawOval(rect, paint);
165 }
166
167 @override
168 void drawPaint(Paint paint) {
169 _main.drawPaint(paint);
170 _screenshot.drawPaint(paint);
171 }
172
173 @override
174 void drawParagraph(ui.Paragraph paragraph, Offset offset) {
175 _main.drawParagraph(paragraph, offset);
176 _screenshot.drawParagraph(paragraph, offset);
177 }
178
179 @override
180 void drawPath(Path path, Paint paint) {
181 _main.drawPath(path, paint);
182 _screenshot.drawPath(path, paint);
183 }
184
185 @override
186 void drawPicture(ui.Picture picture) {
187 _main.drawPicture(picture);
188 _screenshot.drawPicture(picture);
189 }
190
191 @override
192 void drawPoints(ui.PointMode pointMode, List<Offset> points, Paint paint) {
193 _main.drawPoints(pointMode, points, paint);
194 _screenshot.drawPoints(pointMode, points, paint);
195 }
196
197 @override
198 void drawRRect(RRect rrect, Paint paint) {
199 _main.drawRRect(rrect, paint);
200 _screenshot.drawRRect(rrect, paint);
201 }
202
203 @override
204 void drawRawAtlas(ui.Image atlas, Float32List rstTransforms, Float32List rects, Int32List? colors, BlendMode? blendMode, Rect? cullRect, Paint paint) {
205 _main.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
206 _screenshot.drawRawAtlas(atlas, rstTransforms, rects, colors, blendMode, cullRect, paint);
207 }
208
209 @override
210 void drawRawPoints(ui.PointMode pointMode, Float32List points, Paint paint) {
211 _main.drawRawPoints(pointMode, points, paint);
212 _screenshot.drawRawPoints(pointMode, points, paint);
213 }
214
215 @override
216 void drawRect(Rect rect, Paint paint) {
217 _main.drawRect(rect, paint);
218 _screenshot.drawRect(rect, paint);
219 }
220
221 @override
222 void drawShadow(Path path, Color color, double elevation, bool transparentOccluder) {
223 _main.drawShadow(path, color, elevation, transparentOccluder);
224 _screenshot.drawShadow(path, color, elevation, transparentOccluder);
225 }
226
227 @override
228 void drawVertices(ui.Vertices vertices, BlendMode blendMode, Paint paint) {
229 _main.drawVertices(vertices, blendMode, paint);
230 _screenshot.drawVertices(vertices, blendMode, paint);
231 }
232
233 @override
234 int getSaveCount() {
235 // The main canvas is used instead of the screenshot canvas as the main
236 // canvas is guaranteed to be consistent with the canvas expected by the
237 // normal paint pipeline so any logic depending on getSaveCount() will
238 // behave the same as for the regular paint pipeline.
239 return _main.getSaveCount();
240 }
241
242 @override
243 void restore() {
244 _main.restore();
245 _screenshot.restore();
246 }
247
248 @override
249 void rotate(double radians) {
250 _main.rotate(radians);
251 _screenshot.rotate(radians);
252 }
253
254 @override
255 void save() {
256 _main.save();
257 _screenshot.save();
258 }
259
260 @override
261 void saveLayer(Rect? bounds, Paint paint) {
262 _main.saveLayer(bounds, paint);
263 _screenshot.saveLayer(bounds, paint);
264 }
265
266 @override
267 void scale(double sx, [ double? sy ]) {
268 _main.scale(sx, sy);
269 _screenshot.scale(sx, sy);
270 }
271
272 @override
273 void skew(double sx, double sy) {
274 _main.skew(sx, sy);
275 _screenshot.skew(sx, sy);
276 }
277
278 @override
279 void transform(Float64List matrix4) {
280 _main.transform(matrix4);
281 _screenshot.transform(matrix4);
282 }
283
284 @override
285 void translate(double dx, double dy) {
286 _main.translate(dx, dy);
287 _screenshot.translate(dx, dy);
288 }
289
290 @override
291 dynamic noSuchMethod(Invocation invocation) {
292 super.noSuchMethod(invocation);
293 }
294}
295
296Rect _calculateSubtreeBoundsHelper(RenderObject object, Matrix4 transform) {
297 Rect bounds = MatrixUtils.transformRect(transform, object.semanticBounds);
298
299 object.visitChildren((RenderObject child) {
300 final Matrix4 childTransform = transform.clone();
301 object.applyPaintTransform(child, childTransform);
302 Rect childBounds = _calculateSubtreeBoundsHelper(child, childTransform);
303 final Rect? paintClip = object.describeApproximatePaintClip(child);
304 if (paintClip != null) {
305 final Rect transformedPaintClip = MatrixUtils.transformRect(
306 transform,
307 paintClip,
308 );
309 childBounds = childBounds.intersect(transformedPaintClip);
310 }
311
312 if (childBounds.isFinite && !childBounds.isEmpty) {
313 bounds = bounds.isEmpty ? childBounds : bounds.expandToInclude(childBounds);
314 }
315 });
316
317 return bounds;
318}
319
320/// Calculate bounds for a render object and all of its descendants.
321Rect _calculateSubtreeBounds(RenderObject object) {
322 return _calculateSubtreeBoundsHelper(object, Matrix4.identity());
323}
324
325/// A layer that omits its own offset when adding children to the scene so that
326/// screenshots render to the scene in the local coordinate system of the layer.
327class _ScreenshotContainerLayer extends OffsetLayer {
328 @override
329 void addToScene(ui.SceneBuilder builder) {
330 addChildrenToScene(builder);
331 }
332}
333
334/// Data shared between nested [_ScreenshotPaintingContext] objects recording
335/// a screenshot.
336class _ScreenshotData {
337 _ScreenshotData({
338 required this.target,
339 }) : containerLayer = _ScreenshotContainerLayer();
340
341 /// Target to take a screenshot of.
342 final RenderObject target;
343
344 /// Root of the layer tree containing the screenshot.
345 final OffsetLayer containerLayer;
346
347 /// Whether the screenshot target has already been found in the render tree.
348 bool foundTarget = false;
349
350 /// Whether paint operations should record to the screenshot.
351 ///
352 /// At least one of [includeInScreenshot] and [includeInRegularContext] must
353 /// be true.
354 bool includeInScreenshot = false;
355
356 /// Whether paint operations should record to the regular context.
357 ///
358 /// This should only be set to false before paint operations that should only
359 /// apply to the screenshot such rendering debug information about the
360 /// [target].
361 ///
362 /// At least one of [includeInScreenshot] and [includeInRegularContext] must
363 /// be true.
364 bool includeInRegularContext = true;
365
366 /// Offset of the screenshot corresponding to the offset [target] was given as
367 /// part of the regular paint.
368 Offset get screenshotOffset {
369 assert(foundTarget);
370 return containerLayer.offset;
371 }
372 set screenshotOffset(Offset offset) {
373 containerLayer.offset = offset;
374 }
375
376 /// Releases allocated resources.
377 @mustCallSuper
378 void dispose() {
379 containerLayer.dispose();
380 }
381}
382
383/// A place to paint to build screenshots of [RenderObject]s.
384///
385/// Requires that the render objects have already painted successfully as part
386/// of the regular rendering pipeline.
387/// This painting context behaves the same as standard [PaintingContext] with
388/// instrumentation added to compute a screenshot of a specified [RenderObject]
389/// added. To correctly mimic the behavior of the regular rendering pipeline, the
390/// full subtree of the first [RepaintBoundary] ancestor of the specified
391/// [RenderObject] will also be rendered rather than just the subtree of the
392/// render object.
393class _ScreenshotPaintingContext extends PaintingContext {
394 _ScreenshotPaintingContext({
395 required ContainerLayer containerLayer,
396 required Rect estimatedBounds,
397 required _ScreenshotData screenshotData,
398 }) : _data = screenshotData,
399 super(containerLayer, estimatedBounds);
400
401 final _ScreenshotData _data;
402
403 // Recording state
404 PictureLayer? _screenshotCurrentLayer;
405 ui.PictureRecorder? _screenshotRecorder;
406 Canvas? _screenshotCanvas;
407 _MulticastCanvas? _multicastCanvas;
408
409 @override
410 Canvas get canvas {
411 if (_data.includeInScreenshot) {
412 if (_screenshotCanvas == null) {
413 _startRecordingScreenshot();
414 }
415 assert(_screenshotCanvas != null);
416 return _data.includeInRegularContext ? _multicastCanvas! : _screenshotCanvas!;
417 } else {
418 assert(_data.includeInRegularContext);
419 return super.canvas;
420 }
421 }
422
423 bool get _isScreenshotRecording {
424 final bool hasScreenshotCanvas = _screenshotCanvas != null;
425 assert(() {
426 if (hasScreenshotCanvas) {
427 assert(_screenshotCurrentLayer != null);
428 assert(_screenshotRecorder != null);
429 assert(_screenshotCanvas != null);
430 } else {
431 assert(_screenshotCurrentLayer == null);
432 assert(_screenshotRecorder == null);
433 assert(_screenshotCanvas == null);
434 }
435 return true;
436 }());
437 return hasScreenshotCanvas;
438 }
439
440 void _startRecordingScreenshot() {
441 assert(_data.includeInScreenshot);
442 assert(!_isScreenshotRecording);
443 _screenshotCurrentLayer = PictureLayer(estimatedBounds);
444 _screenshotRecorder = ui.PictureRecorder();
445 _screenshotCanvas = Canvas(_screenshotRecorder!);
446 _data.containerLayer.append(_screenshotCurrentLayer!);
447 if (_data.includeInRegularContext) {
448 _multicastCanvas = _MulticastCanvas(
449 main: super.canvas,
450 screenshot: _screenshotCanvas!,
451 );
452 } else {
453 _multicastCanvas = null;
454 }
455 }
456
457 @override
458 void stopRecordingIfNeeded() {
459 super.stopRecordingIfNeeded();
460 _stopRecordingScreenshotIfNeeded();
461 }
462
463 void _stopRecordingScreenshotIfNeeded() {
464 if (!_isScreenshotRecording) {
465 return;
466 }
467 // There is no need to ever draw repaint rainbows as part of the screenshot.
468 _screenshotCurrentLayer!.picture = _screenshotRecorder!.endRecording();
469 _screenshotCurrentLayer = null;
470 _screenshotRecorder = null;
471 _multicastCanvas = null;
472 _screenshotCanvas = null;
473 }
474
475 @override
476 void appendLayer(Layer layer) {
477 if (_data.includeInRegularContext) {
478 super.appendLayer(layer);
479 if (_data.includeInScreenshot) {
480 assert(!_isScreenshotRecording);
481 // We must use a proxy layer here as the layer is already attached to
482 // the regular layer tree.
483 _data.containerLayer.append(_ProxyLayer(layer));
484 }
485 } else {
486 // Only record to the screenshot.
487 assert(!_isScreenshotRecording);
488 assert(_data.includeInScreenshot);
489 layer.remove();
490 _data.containerLayer.append(layer);
491 return;
492 }
493 }
494
495 @override
496 PaintingContext createChildContext(ContainerLayer childLayer, Rect bounds) {
497 if (_data.foundTarget) {
498 // We have already found the screenshotTarget in the layer tree
499 // so we can optimize and use a standard PaintingContext.
500 return super.createChildContext(childLayer, bounds);
501 } else {
502 return _ScreenshotPaintingContext(
503 containerLayer: childLayer,
504 estimatedBounds: bounds,
505 screenshotData: _data,
506 );
507 }
508 }
509
510 @override
511 void paintChild(RenderObject child, Offset offset) {
512 final bool isScreenshotTarget = identical(child, _data.target);
513 if (isScreenshotTarget) {
514 assert(!_data.includeInScreenshot);
515 assert(!_data.foundTarget);
516 _data.foundTarget = true;
517 _data.screenshotOffset = offset;
518 _data.includeInScreenshot = true;
519 }
520 super.paintChild(child, offset);
521 if (isScreenshotTarget) {
522 _stopRecordingScreenshotIfNeeded();
523 _data.includeInScreenshot = false;
524 }
525 }
526
527 /// Captures an image of the current state of [renderObject] and its children.
528 ///
529 /// The returned [ui.Image] has uncompressed raw RGBA bytes, will be offset
530 /// by the top-left corner of [renderBounds], and have dimensions equal to the
531 /// size of [renderBounds] multiplied by [pixelRatio].
532 ///
533 /// To use [toImage], the render object must have gone through the paint phase
534 /// (i.e. [debugNeedsPaint] must be false).
535 ///
536 /// The [pixelRatio] describes the scale between the logical pixels and the
537 /// size of the output image. It is independent of the
538 /// [window.devicePixelRatio] for the device, so specifying 1.0 (the default)
539 /// will give you a 1:1 mapping between logical pixels and the output pixels
540 /// in the image.
541 ///
542 /// The [debugPaint] argument specifies whether the image should include the
543 /// output of [RenderObject.debugPaint] for [renderObject] with
544 /// [debugPaintSizeEnabled] set to true. Debug paint information is not
545 /// included for the children of [renderObject] so that it is clear precisely
546 /// which object the debug paint information references.
547 ///
548 /// See also:
549 ///
550 /// * [RenderRepaintBoundary.toImage] for a similar API for [RenderObject]s
551 /// that are repaint boundaries that can be used outside of the inspector.
552 /// * [OffsetLayer.toImage] for a similar API at the layer level.
553 /// * [dart:ui.Scene.toImage] for more information about the image returned.
554 static Future<ui.Image> toImage(
555 RenderObject renderObject,
556 Rect renderBounds, {
557 double pixelRatio = 1.0,
558 bool debugPaint = false,
559 }) async {
560 RenderObject repaintBoundary = renderObject;
561 while (!repaintBoundary.isRepaintBoundary) {
562 repaintBoundary = repaintBoundary.parent!;
563 }
564 final _ScreenshotData data = _ScreenshotData(target: renderObject);
565 final _ScreenshotPaintingContext context = _ScreenshotPaintingContext(
566 containerLayer: repaintBoundary.debugLayer!,
567 estimatedBounds: repaintBoundary.paintBounds,
568 screenshotData: data,
569 );
570
571 if (identical(renderObject, repaintBoundary)) {
572 // Painting the existing repaint boundary to the screenshot is sufficient.
573 // We don't just take a direct screenshot of the repaint boundary as we
574 // want to capture debugPaint information as well.
575 data.containerLayer.append(_ProxyLayer(repaintBoundary.debugLayer!));
576 data.foundTarget = true;
577 final OffsetLayer offsetLayer = repaintBoundary.debugLayer! as OffsetLayer;
578 data.screenshotOffset = offsetLayer.offset;
579 } else {
580 // Repaint everything under the repaint boundary.
581 // We call debugInstrumentRepaintCompositedChild instead of paintChild as
582 // we need to force everything under the repaint boundary to repaint.
583 PaintingContext.debugInstrumentRepaintCompositedChild(
584 repaintBoundary,
585 customContext: context,
586 );
587 }
588
589 // The check that debugPaintSizeEnabled is false exists to ensure we only
590 // call debugPaint when it wasn't already called.
591 if (debugPaint && !debugPaintSizeEnabled) {
592 data.includeInRegularContext = false;
593 // Existing recording may be to a canvas that draws to both the normal and
594 // screenshot canvases.
595 context.stopRecordingIfNeeded();
596 assert(data.foundTarget);
597 data.includeInScreenshot = true;
598
599 debugPaintSizeEnabled = true;
600 try {
601 renderObject.debugPaint(context, data.screenshotOffset);
602 } finally {
603 debugPaintSizeEnabled = false;
604 context.stopRecordingIfNeeded();
605 }
606 }
607
608 // We must build the regular scene before we can build the screenshot
609 // scene as building the screenshot scene assumes addToScene has already
610 // been called successfully for all layers in the regular scene.
611 repaintBoundary.debugLayer!.buildScene(ui.SceneBuilder());
612
613 final ui.Image image;
614
615 try {
616 image = await data.containerLayer.toImage(renderBounds, pixelRatio: pixelRatio);
617 } finally {
618 data.dispose();
619 }
620
621 return image;
622 }
623}
624
625/// A class describing a step along a path through a tree of [DiagnosticsNode]
626/// objects.
627///
628/// This class is used to bundle all data required to display the tree with just
629/// the nodes along a path expanded into a single JSON payload.
630class _DiagnosticsPathNode {
631 /// Creates a full description of a step in a path through a tree of
632 /// [DiagnosticsNode] objects.
633 _DiagnosticsPathNode({
634 required this.node,
635 required this.children,
636 this.childIndex,
637 });
638
639 /// Node at the point in the path this [_DiagnosticsPathNode] is describing.
640 final DiagnosticsNode node;
641
642 /// Children of the [node] being described.
643 ///
644 /// This value is cached instead of relying on `node.getChildren()` as that
645 /// method call might create new [DiagnosticsNode] objects for each child
646 /// and we would prefer to use the identical [DiagnosticsNode] for each time
647 /// a node exists in the path.
648 final List<DiagnosticsNode> children;
649
650 /// Index of the child that the path continues on.
651 ///
652 /// Equal to null if the path does not continue.
653 final int? childIndex;
654}
655
656List<_DiagnosticsPathNode>? _followDiagnosticableChain(
657 List<Diagnosticable> chain, {
658 String? name,
659 DiagnosticsTreeStyle? style,
660}) {
661 final List<_DiagnosticsPathNode> path = <_DiagnosticsPathNode>[];
662 if (chain.isEmpty) {
663 return path;
664 }
665 DiagnosticsNode diagnostic = chain.first.toDiagnosticsNode(name: name, style: style);
666 for (int i = 1; i < chain.length; i += 1) {
667 final Diagnosticable target = chain[i];
668 bool foundMatch = false;
669 final List<DiagnosticsNode> children = diagnostic.getChildren();
670 for (int j = 0; j < children.length; j += 1) {
671 final DiagnosticsNode child = children[j];
672 if (child.value == target) {
673 foundMatch = true;
674 path.add(_DiagnosticsPathNode(
675 node: diagnostic,
676 children: children,
677 childIndex: j,
678 ));
679 diagnostic = child;
680 break;
681 }
682 }
683 assert(foundMatch);
684 }
685 path.add(_DiagnosticsPathNode(node: diagnostic, children: diagnostic.getChildren()));
686 return path;
687}
688
689/// Signature for the selection change callback used by
690/// [WidgetInspectorService.selectionChangedCallback].
691typedef InspectorSelectionChangedCallback = void Function();
692
693/// Structure to help reference count Dart objects referenced by a GUI tool
694/// using [WidgetInspectorService].
695///
696/// Does not hold the object from garbage collection.
697@visibleForTesting
698class InspectorReferenceData {
699 /// Creates an instance of [InspectorReferenceData].
700 InspectorReferenceData(Object object, this.id) {
701 // These types are not supported by [WeakReference].
702 // See https://api.dart.dev/stable/3.0.2/dart-core/WeakReference-class.html
703 if (object is String || object is num || object is bool) {
704 _value = object;
705 return;
706 }
707
708 _ref = WeakReference<Object>(object);
709 }
710
711 WeakReference<Object>? _ref;
712
713 Object? _value;
714
715 /// The id of the object in the widget inspector records.
716 final String id;
717
718 /// The number of times the object has been referenced.
719 int count = 1;
720
721 /// The value.
722 Object? get value {
723 if (_ref != null) {
724 return _ref!.target;
725 }
726 return _value;
727 }
728}
729
730// Production implementation of [WidgetInspectorService].
731class _WidgetInspectorService with WidgetInspectorService {
732 _WidgetInspectorService() {
733 selection.addListener(() {
734 if (selectionChangedCallback != null) {
735 selectionChangedCallback!();
736 }
737 });
738 }
739}
740
741/// Service used by GUI tools to interact with the [WidgetInspector].
742///
743/// Calls to this object are typically made from GUI tools such as the [Flutter
744/// IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
745/// using the [Dart VM Service](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md).
746/// This class uses its own object id and manages object lifecycles itself
747/// instead of depending on the [object ids](https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#getobject)
748/// specified by the VM Service Protocol because the VM Service Protocol ids
749/// expire unpredictably. Object references are tracked in groups so that tools
750/// that clients can use dereference all objects in a group with a single
751/// operation making it easier to avoid memory leaks.
752///
753/// All methods in this class are appropriate to invoke from debugging tools
754/// using the VM service protocol to evaluate Dart expressions of the
755/// form `WidgetInspectorService.instance.methodName(arg1, arg2, ...)`. If you
756/// make changes to any instance method of this class you need to verify that
757/// the [Flutter IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md)
758/// widget inspector support still works with the changes.
759///
760/// All methods returning String values return JSON.
761mixin WidgetInspectorService {
762 /// Ring of cached JSON values to prevent JSON from being garbage
763 /// collected before it can be requested over the VM service protocol.
764 final List<String?> _serializeRing = List<String?>.filled(20, null);
765 int _serializeRingIndex = 0;
766
767 /// The current [WidgetInspectorService].
768 static WidgetInspectorService get instance => _instance;
769 static WidgetInspectorService _instance = _WidgetInspectorService();
770
771 /// Whether the inspector is in select mode.
772 ///
773 /// In select mode, pointer interactions trigger widget selection instead of
774 /// normal interactions. Otherwise the previously selected widget is
775 /// highlighted but the application can be interacted with normally.
776 @visibleForTesting
777 final ValueNotifier<bool> isSelectMode = ValueNotifier<bool>(true);
778
779 @protected
780 static set instance(WidgetInspectorService instance) {
781 _instance = instance;
782 }
783
784 static bool _debugServiceExtensionsRegistered = false;
785
786 /// Ground truth tracking what object(s) are currently selected used by both
787 /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector]
788 /// displayed on the device.
789 final InspectorSelection selection = InspectorSelection();
790
791 /// Callback typically registered by the [WidgetInspector] to receive
792 /// notifications when [selection] changes.
793 ///
794 /// The Flutter IntelliJ Plugin does not need to listen for this event as it
795 /// instead listens for `dart:developer` `inspect` events which also trigger
796 /// when the inspection target changes on device.
797 InspectorSelectionChangedCallback? selectionChangedCallback;
798
799 /// The VM service protocol does not keep alive object references so this
800 /// class needs to manually manage groups of objects that should be kept
801 /// alive.
802 final Map<String, Set<InspectorReferenceData>> _groups = <String, Set<InspectorReferenceData>>{};
803 final Map<String, InspectorReferenceData> _idToReferenceData = <String, InspectorReferenceData>{};
804 final WeakMap<Object, String> _objectToId = WeakMap<Object, String>();
805 int _nextId = 0;
806
807 /// The pubRootDirectories that are currently configured for the widget inspector.
808 List<String>? _pubRootDirectories;
809
810 /// Memoization for [_isLocalCreationLocation].
811 final HashMap<String, bool> _isLocalCreationCache = HashMap<String, bool>();
812
813 bool _trackRebuildDirtyWidgets = false;
814 bool _trackRepaintWidgets = false;
815
816 /// Registers a service extension method with the given name (full
817 /// name "ext.flutter.inspector.name").
818 ///
819 /// The given callback is called when the extension method is called. The
820 /// callback must return a value that can be converted to JSON using
821 /// `json.encode()` (see [JsonEncoder]). The return value is stored as a
822 /// property named `result` in the JSON. In case of failure, the failure is
823 /// reported to the remote caller and is dumped to the logs.
824 @protected
825 void registerServiceExtension({
826 required String name,
827 required ServiceExtensionCallback callback,
828 required RegisterServiceExtensionCallback registerExtension,
829 }) {
830 registerExtension(
831 name: 'inspector.$name',
832 callback: callback,
833 );
834 }
835
836 /// Registers a service extension method with the given name (full
837 /// name "ext.flutter.inspector.name"), which takes no arguments.
838 void _registerSignalServiceExtension({
839 required String name,
840 required FutureOr<Object?> Function() callback,
841 required RegisterServiceExtensionCallback registerExtension,
842 }) {
843 registerServiceExtension(
844 name: name,
845 callback: (Map<String, String> parameters) async {
846 return <String, Object?>{'result': await callback()};
847 },
848 registerExtension: registerExtension,
849 );
850 }
851
852 /// Registers a service extension method with the given name (full
853 /// name "ext.flutter.inspector.name"), which takes a single optional argument
854 /// "objectGroup" specifying what group is used to manage lifetimes of
855 /// object references in the returned JSON (see [disposeGroup]).
856 /// If "objectGroup" is omitted, the returned JSON will not include any object
857 /// references to avoid leaking memory.
858 void _registerObjectGroupServiceExtension({
859 required String name,
860 required FutureOr<Object?> Function(String objectGroup) callback,
861 required RegisterServiceExtensionCallback registerExtension,
862 }) {
863 registerServiceExtension(
864 name: name,
865 callback: (Map<String, String> parameters) async {
866 return <String, Object?>{'result': await callback(parameters['objectGroup']!)};
867 },
868 registerExtension: registerExtension,
869 );
870 }
871
872 /// Registers a service extension method with the given name (full
873 /// name "ext.flutter.inspector.name"), which takes a single argument
874 /// "enabled" which can have the value "true" or the value "false"
875 /// or can be omitted to read the current value. (Any value other
876 /// than "true" is considered equivalent to "false". Other arguments
877 /// are ignored.)
878 ///
879 /// Calls the `getter` callback to obtain the value when
880 /// responding to the service extension method being called.
881 ///
882 /// Calls the `setter` callback with the new value when the
883 /// service extension method is called with a new value.
884 void _registerBoolServiceExtension({
885 required String name,
886 required AsyncValueGetter<bool> getter,
887 required AsyncValueSetter<bool> setter,
888 required RegisterServiceExtensionCallback registerExtension,
889 }) {
890 registerServiceExtension(
891 name: name,
892 callback: (Map<String, String> parameters) async {
893 if (parameters.containsKey('enabled')) {
894 final bool value = parameters['enabled'] == 'true';
895 await setter(value);
896 _postExtensionStateChangedEvent(name, value);
897 }
898 return <String, dynamic>{'enabled': await getter() ? 'true' : 'false'};
899 },
900 registerExtension: registerExtension,
901 );
902 }
903
904 /// Sends an event when a service extension's state is changed.
905 ///
906 /// Clients should listen for this event to stay aware of the current service
907 /// extension state. Any service extension that manages a state should call
908 /// this method on state change.
909 ///
910 /// `value` reflects the newly updated service extension value.
911 ///
912 /// This will be called automatically for service extensions registered via
913 /// [registerBoolServiceExtension].
914 void _postExtensionStateChangedEvent(String name, Object? value) {
915 postEvent(
916 'Flutter.ServiceExtensionStateChanged',
917 <String, Object?>{
918 'extension': 'ext.flutter.inspector.$name',
919 'value': value,
920 },
921 );
922 }
923
924 /// Registers a service extension method with the given name (full
925 /// name "ext.flutter.inspector.name") which takes an optional parameter named
926 /// "arg" and a required parameter named "objectGroup" used to control the
927 /// lifetimes of object references in the returned JSON (see [disposeGroup]).
928 void _registerServiceExtensionWithArg({
929 required String name,
930 required FutureOr<Object?> Function(String? objectId, String objectGroup) callback,
931 required RegisterServiceExtensionCallback registerExtension,
932 }) {
933 registerServiceExtension(
934 name: name,
935 callback: (Map<String, String> parameters) async {
936 assert(parameters.containsKey('objectGroup'));
937 return <String, Object?>{
938 'result': await callback(parameters['arg'], parameters['objectGroup']!),
939 };
940 },
941 registerExtension: registerExtension,
942 );
943 }
944
945 /// Registers a service extension method with the given name (full
946 /// name "ext.flutter.inspector.name"), that takes arguments
947 /// "arg0", "arg1", "arg2", ..., "argn".
948 void _registerServiceExtensionVarArgs({
949 required String name,
950 required FutureOr<Object?> Function(List<String> args) callback,
951 required RegisterServiceExtensionCallback registerExtension,
952 }) {
953 registerServiceExtension(
954 name: name,
955 callback: (Map<String, String> parameters) async {
956 final List<String> args = <String>[];
957 int index = 0;
958 while (true) {
959 final String name = 'arg$index';
960 if (parameters.containsKey(name)) {
961 args.add(parameters[name]!);
962 } else {
963 break;
964 }
965 index++;
966 }
967 // Verify that the only arguments other than perhaps 'isolateId' are
968 // arguments we have already handled.
969 assert(index == parameters.length || (index == parameters.length - 1 && parameters.containsKey('isolateId')));
970 return <String, Object?>{'result': await callback(args)};
971 },
972 registerExtension: registerExtension,
973 );
974 }
975
976 /// Cause the entire tree to be rebuilt. This is used by development tools
977 /// when the application code has changed and is being hot-reloaded, to cause
978 /// the widget tree to pick up any changed implementations.
979 ///
980 /// This is expensive and should not be called except during development.
981 @protected
982 Future<void> forceRebuild() {
983 final WidgetsBinding binding = WidgetsBinding.instance;
984 if (binding.rootElement != null) {
985 binding.buildOwner!.reassemble(binding.rootElement!);
986 return binding.endOfFrame;
987 }
988 return Future<void>.value();
989 }
990
991 static const String _consoleObjectGroup = 'console-group';
992
993 int _errorsSinceReload = 0;
994
995 void _reportStructuredError(FlutterErrorDetails details) {
996 final Map<String, Object?> errorJson = _nodeToJson(
997 details.toDiagnosticsNode(),
998 InspectorSerializationDelegate(
999 groupName: _consoleObjectGroup,
1000 subtreeDepth: 5,
1001 includeProperties: true,
1002 maxDescendantsTruncatableNode: 5,
1003 service: this,
1004 ),
1005 )!;
1006
1007 errorJson['errorsSinceReload'] = _errorsSinceReload;
1008 if (_errorsSinceReload == 0) {
1009 errorJson['renderedErrorText'] = TextTreeRenderer(
1010 wrapWidthProperties: FlutterError.wrapWidth,
1011 maxDescendentsTruncatableNode: 5,
1012 ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight();
1013 } else {
1014 errorJson['renderedErrorText'] = 'Another exception was thrown: ${details.summary}';
1015 }
1016
1017 _errorsSinceReload += 1;
1018 postEvent('Flutter.Error', errorJson);
1019 }
1020
1021 /// Resets the count of errors since the last hot reload.
1022 ///
1023 /// This data is sent to clients as part of the 'Flutter.Error' service
1024 /// protocol event. Clients may choose to display errors received after the
1025 /// first error differently.
1026 void _resetErrorCount() {
1027 _errorsSinceReload = 0;
1028 }
1029
1030 /// Whether structured errors are enabled.
1031 ///
1032 /// Structured errors provide semantic information that can be used by IDEs
1033 /// to enhance the display of errors with rich formatting.
1034 bool isStructuredErrorsEnabled() {
1035 // This is a debug mode only feature and will default to false for
1036 // profile mode.
1037 bool enabled = false;
1038 assert(() {
1039 // TODO(kenz): add support for structured errors on the web.
1040 enabled = const bool.fromEnvironment('flutter.inspector.structuredErrors', defaultValue: !kIsWeb);
1041 return true;
1042 }());
1043 return enabled;
1044 }
1045
1046 /// Called to register service extensions.
1047 ///
1048 /// See also:
1049 ///
1050 /// * <https://github.com/dart-lang/sdk/blob/main/runtime/vm/service/service.md#rpcs-requests-and-responses>
1051 /// * [BindingBase.initServiceExtensions], which explains when service
1052 /// extensions can be used.
1053 void initServiceExtensions(RegisterServiceExtensionCallback registerExtension) {
1054 final FlutterExceptionHandler defaultExceptionHandler = FlutterError.presentError;
1055
1056 if (isStructuredErrorsEnabled()) {
1057 FlutterError.presentError = _reportStructuredError;
1058 }
1059 assert(!_debugServiceExtensionsRegistered);
1060 assert(() {
1061 _debugServiceExtensionsRegistered = true;
1062 return true;
1063 }());
1064
1065 SchedulerBinding.instance.addPersistentFrameCallback(_onFrameStart);
1066
1067 _registerBoolServiceExtension(
1068 name: WidgetInspectorServiceExtensions.structuredErrors.name,
1069 getter: () async => FlutterError.presentError == _reportStructuredError,
1070 setter: (bool value) {
1071 FlutterError.presentError = value ? _reportStructuredError : defaultExceptionHandler;
1072 return Future<void>.value();
1073 },
1074 registerExtension: registerExtension,
1075 );
1076
1077 _registerBoolServiceExtension(
1078 name: WidgetInspectorServiceExtensions.show.name,
1079 getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
1080 setter: (bool value) {
1081 if (WidgetsApp.debugShowWidgetInspectorOverride != value) {
1082 WidgetsApp.debugShowWidgetInspectorOverride = value;
1083 }
1084 return Future<void>.value();
1085 },
1086 registerExtension: registerExtension,
1087 );
1088
1089 if (isWidgetCreationTracked()) {
1090 // Service extensions that are only supported if widget creation locations
1091 // are tracked.
1092 _registerBoolServiceExtension(
1093 name: WidgetInspectorServiceExtensions.trackRebuildDirtyWidgets.name,
1094 getter: () async => _trackRebuildDirtyWidgets,
1095 setter: (bool value) async {
1096 if (value == _trackRebuildDirtyWidgets) {
1097 return;
1098 }
1099 _rebuildStats.resetCounts();
1100 _trackRebuildDirtyWidgets = value;
1101 if (value) {
1102 assert(debugOnRebuildDirtyWidget == null);
1103 debugOnRebuildDirtyWidget = _onRebuildWidget;
1104 // Trigger a rebuild so there are baseline stats for rebuilds
1105 // performed by the app.
1106 await forceRebuild();
1107 return;
1108 } else {
1109 debugOnRebuildDirtyWidget = null;
1110 return;
1111 }
1112 },
1113 registerExtension: registerExtension,
1114 );
1115
1116 _registerBoolServiceExtension(
1117 name: WidgetInspectorServiceExtensions.trackRepaintWidgets.name,
1118 getter: () async => _trackRepaintWidgets,
1119 setter: (bool value) async {
1120 if (value == _trackRepaintWidgets) {
1121 return;
1122 }
1123 _repaintStats.resetCounts();
1124 _trackRepaintWidgets = value;
1125 if (value) {
1126 assert(debugOnProfilePaint == null);
1127 debugOnProfilePaint = _onPaint;
1128 // Trigger an immediate paint so the user has some baseline painting
1129 // stats to view.
1130 void markTreeNeedsPaint(RenderObject renderObject) {
1131 renderObject.markNeedsPaint();
1132 renderObject.visitChildren(markTreeNeedsPaint);
1133 }
1134 RendererBinding.instance.renderViews.forEach(markTreeNeedsPaint);
1135 } else {
1136 debugOnProfilePaint = null;
1137 }
1138 },
1139 registerExtension: registerExtension,
1140 );
1141 }
1142
1143 _registerSignalServiceExtension(
1144 name: WidgetInspectorServiceExtensions.disposeAllGroups.name,
1145 callback: () async {
1146 disposeAllGroups();
1147 return null;
1148 },
1149 registerExtension: registerExtension,
1150 );
1151 _registerObjectGroupServiceExtension(
1152 name: WidgetInspectorServiceExtensions.disposeGroup.name,
1153 callback: (String name) async {
1154 disposeGroup(name);
1155 return null;
1156 },
1157 registerExtension: registerExtension,
1158 );
1159 _registerSignalServiceExtension(
1160 name: WidgetInspectorServiceExtensions.isWidgetTreeReady.name,
1161 callback: isWidgetTreeReady,
1162 registerExtension: registerExtension,
1163 );
1164 _registerServiceExtensionWithArg(
1165 name: WidgetInspectorServiceExtensions.disposeId.name,
1166 callback: (String? objectId, String objectGroup) async {
1167 disposeId(objectId, objectGroup);
1168 return null;
1169 },
1170 registerExtension: registerExtension,
1171 );
1172 _registerServiceExtensionVarArgs(
1173 name: WidgetInspectorServiceExtensions.setPubRootDirectories.name,
1174 callback: (List<String> args) async {
1175 setPubRootDirectories(args);
1176 return null;
1177 },
1178 registerExtension: registerExtension,
1179 );
1180 _registerServiceExtensionVarArgs(
1181 name: WidgetInspectorServiceExtensions.addPubRootDirectories.name,
1182 callback: (List<String> args) async {
1183 addPubRootDirectories(args);
1184 return null;
1185 },
1186 registerExtension: registerExtension,
1187 );
1188 _registerServiceExtensionVarArgs(
1189 name: WidgetInspectorServiceExtensions.removePubRootDirectories.name,
1190 callback: (List<String> args) async {
1191 removePubRootDirectories(args);
1192 return null;
1193 },
1194 registerExtension: registerExtension,
1195 );
1196 registerServiceExtension(
1197 name: WidgetInspectorServiceExtensions.getPubRootDirectories.name,
1198 callback: pubRootDirectories,
1199 registerExtension: registerExtension,
1200 );
1201 _registerServiceExtensionWithArg(
1202 name: WidgetInspectorServiceExtensions.setSelectionById.name,
1203 callback: setSelectionById,
1204 registerExtension: registerExtension,
1205 );
1206 _registerServiceExtensionWithArg(
1207 name: WidgetInspectorServiceExtensions.getParentChain.name,
1208 callback: _getParentChain,
1209 registerExtension: registerExtension,
1210 );
1211 _registerServiceExtensionWithArg(
1212 name: WidgetInspectorServiceExtensions.getProperties.name,
1213 callback: _getProperties,
1214 registerExtension: registerExtension,
1215 );
1216 _registerServiceExtensionWithArg(
1217 name: WidgetInspectorServiceExtensions.getChildren.name,
1218 callback: _getChildren,
1219 registerExtension: registerExtension,
1220 );
1221
1222 _registerServiceExtensionWithArg(
1223 name: WidgetInspectorServiceExtensions.getChildrenSummaryTree.name,
1224 callback: _getChildrenSummaryTree,
1225 registerExtension: registerExtension,
1226 );
1227
1228 _registerServiceExtensionWithArg(
1229 name: WidgetInspectorServiceExtensions.getChildrenDetailsSubtree.name,
1230 callback: _getChildrenDetailsSubtree,
1231 registerExtension: registerExtension,
1232 );
1233
1234 _registerObjectGroupServiceExtension(
1235 name: WidgetInspectorServiceExtensions.getRootWidget.name,
1236 callback: _getRootWidget,
1237 registerExtension: registerExtension,
1238 );
1239 _registerObjectGroupServiceExtension(
1240 name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTree.name,
1241 callback: _getRootWidgetSummaryTree,
1242 registerExtension: registerExtension,
1243 );
1244 registerServiceExtension(
1245 name: WidgetInspectorServiceExtensions.getRootWidgetSummaryTreeWithPreviews.name,
1246 callback: _getRootWidgetSummaryTreeWithPreviews,
1247 registerExtension: registerExtension,
1248 );
1249 registerServiceExtension(
1250 name: WidgetInspectorServiceExtensions.getDetailsSubtree.name,
1251 callback: (Map<String, String> parameters) async {
1252 assert(parameters.containsKey('objectGroup'));
1253 final String? subtreeDepth = parameters['subtreeDepth'];
1254 return <String, Object?>{
1255 'result': _getDetailsSubtree(
1256 parameters['arg'],
1257 parameters['objectGroup'],
1258 subtreeDepth != null ? int.parse(subtreeDepth) : 2,
1259 ),
1260 };
1261 },
1262 registerExtension: registerExtension,
1263 );
1264 _registerServiceExtensionWithArg(
1265 name: WidgetInspectorServiceExtensions.getSelectedWidget.name,
1266 callback: _getSelectedWidget,
1267 registerExtension: registerExtension,
1268 );
1269 _registerServiceExtensionWithArg(
1270 name: WidgetInspectorServiceExtensions.getSelectedSummaryWidget.name,
1271 callback: _getSelectedSummaryWidget,
1272 registerExtension: registerExtension,
1273 );
1274
1275 _registerSignalServiceExtension(
1276 name: WidgetInspectorServiceExtensions.isWidgetCreationTracked.name,
1277 callback: isWidgetCreationTracked,
1278 registerExtension: registerExtension,
1279 );
1280 registerServiceExtension(
1281 name: WidgetInspectorServiceExtensions.screenshot.name,
1282 callback: (Map<String, String> parameters) async {
1283 assert(parameters.containsKey('id'));
1284 assert(parameters.containsKey('width'));
1285 assert(parameters.containsKey('height'));
1286
1287 final ui.Image? image = await screenshot(
1288 toObject(parameters['id']),
1289 width: double.parse(parameters['width']!),
1290 height: double.parse(parameters['height']!),
1291 margin: parameters.containsKey('margin') ?
1292 double.parse(parameters['margin']!) : 0.0,
1293 maxPixelRatio: parameters.containsKey('maxPixelRatio') ?
1294 double.parse(parameters['maxPixelRatio']!) : 1.0,
1295 debugPaint: parameters['debugPaint'] == 'true',
1296 );
1297 if (image == null) {
1298 return <String, Object?>{'result': null};
1299 }
1300 final ByteData? byteData = await image.toByteData(format:ui.ImageByteFormat.png);
1301 image.dispose();
1302
1303 return <String, Object>{
1304 'result': base64.encoder.convert(Uint8List.view(byteData!.buffer)),
1305 };
1306 },
1307 registerExtension: registerExtension,
1308 );
1309 registerServiceExtension(
1310 name: WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
1311 callback: _getLayoutExplorerNode,
1312 registerExtension: registerExtension,
1313 );
1314 registerServiceExtension(
1315 name: WidgetInspectorServiceExtensions.setFlexFit.name,
1316 callback: _setFlexFit,
1317 registerExtension: registerExtension,
1318 );
1319 registerServiceExtension(
1320 name: WidgetInspectorServiceExtensions.setFlexFactor.name,
1321 callback: _setFlexFactor,
1322 registerExtension: registerExtension,
1323 );
1324 registerServiceExtension(
1325 name: WidgetInspectorServiceExtensions.setFlexProperties.name,
1326 callback: _setFlexProperties,
1327 registerExtension: registerExtension,
1328 );
1329 }
1330
1331 void _clearStats() {
1332 _rebuildStats.resetCounts();
1333 _repaintStats.resetCounts();
1334 }
1335
1336 /// Clear all InspectorService object references.
1337 ///
1338 /// Use this method only for testing to ensure that object references from one
1339 /// test case do not impact other test cases.
1340 @visibleForTesting
1341 @protected
1342 void disposeAllGroups() {
1343 _groups.clear();
1344 _idToReferenceData.clear();
1345 _objectToId.clear();
1346 _nextId = 0;
1347 }
1348
1349 /// Reset all InspectorService state.
1350 ///
1351 /// Use this method only for testing to write hermetic tests for
1352 /// WidgetInspectorService.
1353 @visibleForTesting
1354 @protected
1355 @mustCallSuper
1356 void resetAllState() {
1357 disposeAllGroups();
1358 selection.clear();
1359 resetPubRootDirectories();
1360 }
1361
1362 /// Free all references to objects in a group.
1363 ///
1364 /// Objects and their associated ids in the group may be kept alive by
1365 /// references from a different group.
1366 @protected
1367 void disposeGroup(String name) {
1368 final Set<InspectorReferenceData>? references = _groups.remove(name);
1369 if (references == null) {
1370 return;
1371 }
1372 references.forEach(_decrementReferenceCount);
1373 }
1374
1375 void _decrementReferenceCount(InspectorReferenceData reference) {
1376 reference.count -= 1;
1377 assert(reference.count >= 0);
1378 if (reference.count == 0) {
1379 final Object? value = reference.value;
1380 if (value != null) {
1381 _objectToId.remove(value);
1382 }
1383 _idToReferenceData.remove(reference.id);
1384 }
1385 }
1386
1387 /// Returns a unique id for [object] that will remain live at least until
1388 /// [disposeGroup] is called on [groupName].
1389 @protected
1390 String? toId(Object? object, String groupName) {
1391 if (object == null) {
1392 return null;
1393 }
1394
1395 final Set<InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => Set<InspectorReferenceData>.identity());
1396 String? id = _objectToId[object];
1397 InspectorReferenceData referenceData;
1398 if (id == null) {
1399 // TODO(polina-c): comment here why we increase memory footprint by the prefix 'inspector-'.
1400 // https://github.com/flutter/devtools/issues/5995
1401 id = 'inspector-$_nextId';
1402 _nextId += 1;
1403 _objectToId[object] = id;
1404 referenceData = InspectorReferenceData(object, id);
1405 _idToReferenceData[id] = referenceData;
1406 group.add(referenceData);
1407 } else {
1408 referenceData = _idToReferenceData[id]!;
1409 if (group.add(referenceData)) {
1410 referenceData.count += 1;
1411 }
1412 }
1413 return id;
1414 }
1415
1416 /// Returns whether the application has rendered its first frame and it is
1417 /// appropriate to display the Widget tree in the inspector.
1418 @protected
1419 bool isWidgetTreeReady([ String? groupName ]) {
1420 return WidgetsBinding.instance.debugDidSendFirstFrameEvent;
1421 }
1422
1423 /// Returns the Dart object associated with a reference id.
1424 ///
1425 /// The `groupName` parameter is not required by is added to regularize the
1426 /// API surface of the methods in this class called from the Flutter IntelliJ
1427 /// Plugin.
1428 @protected
1429 Object? toObject(String? id, [ String? groupName ]) {
1430 if (id == null) {
1431 return null;
1432 }
1433
1434 final InspectorReferenceData? data = _idToReferenceData[id];
1435 if (data == null) {
1436 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist.')]);
1437 }
1438 return data.value;
1439 }
1440
1441 /// Returns the object to introspect to determine the source location of an
1442 /// object's class.
1443 ///
1444 /// The Dart object for the id is returned for all cases but [Element] objects
1445 /// where the [Widget] configuring the [Element] is returned instead as the
1446 /// class of the [Widget] is more relevant than the class of the [Element].
1447 ///
1448 /// The `groupName` parameter is not required by is added to regularize the
1449 /// API surface of methods called from the Flutter IntelliJ Plugin.
1450 @protected
1451 Object? toObjectForSourceLocation(String id, [ String? groupName ]) {
1452 final Object? object = toObject(id);
1453 if (object is Element) {
1454 return object.widget;
1455 }
1456 return object;
1457 }
1458
1459 /// Remove the object with the specified `id` from the specified object
1460 /// group.
1461 ///
1462 /// If the object exists in other groups it will remain alive and the object
1463 /// id will remain valid.
1464 @protected
1465 void disposeId(String? id, String groupName) {
1466 if (id == null) {
1467 return;
1468 }
1469
1470 final InspectorReferenceData? referenceData = _idToReferenceData[id];
1471 if (referenceData == null) {
1472 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id does not exist')]);
1473 }
1474 if (_groups[groupName]?.remove(referenceData) != true) {
1475 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Id is not in group')]);
1476 }
1477 _decrementReferenceCount(referenceData);
1478 }
1479
1480 /// Set the list of directories that should be considered part of the local
1481 /// project.
1482 ///
1483 /// The local project directories are used to distinguish widgets created by
1484 /// the local project from widgets created from inside the framework
1485 /// or other packages.
1486 @protected
1487 @Deprecated(
1488 'Use addPubRootDirectories instead. '
1489 'This feature was deprecated after v3.18.0-2.0.pre.',
1490 )
1491 void setPubRootDirectories(List<String> pubRootDirectories) {
1492 addPubRootDirectories(pubRootDirectories);
1493 }
1494
1495 /// Resets the list of directories, that should be considered part of the
1496 /// local project, to the value passed in [pubRootDirectories].
1497 ///
1498 /// The local project directories are used to distinguish widgets created by
1499 /// the local project from widgets created from inside the framework
1500 /// or other packages.
1501 @visibleForTesting
1502 @protected
1503 void resetPubRootDirectories() {
1504 _pubRootDirectories = <String>[];
1505 _isLocalCreationCache.clear();
1506 }
1507
1508 /// Add a list of directories that should be considered part of the local
1509 /// project.
1510 ///
1511 /// The local project directories are used to distinguish widgets created by
1512 /// the local project from widgets created from inside the framework
1513 /// or other packages.
1514 @protected
1515 void addPubRootDirectories(List<String> pubRootDirectories) {
1516 pubRootDirectories = pubRootDirectories.map<String>((String directory) => Uri.parse(directory).path).toList();
1517
1518 final Set<String> directorySet = Set<String>.from(pubRootDirectories);
1519 if (_pubRootDirectories != null) {
1520 directorySet.addAll(_pubRootDirectories!);
1521 }
1522
1523 _pubRootDirectories = directorySet.toList();
1524 _isLocalCreationCache.clear();
1525 }
1526
1527 /// Remove a list of directories that should no longer be considered part
1528 /// of the local project.
1529 ///
1530 /// The local project directories are used to distinguish widgets created by
1531 /// the local project from widgets created from inside the framework
1532 /// or other packages.
1533 @protected
1534 void removePubRootDirectories(List<String> pubRootDirectories) {
1535 if (_pubRootDirectories == null) {
1536 return;
1537 }
1538 pubRootDirectories = pubRootDirectories.map<String>((String directory) => Uri.parse(directory).path).toList();
1539
1540 final Set<String> directorySet = Set<String>.from(_pubRootDirectories!);
1541 directorySet.removeAll(pubRootDirectories);
1542
1543 _pubRootDirectories = directorySet.toList();
1544 _isLocalCreationCache.clear();
1545 }
1546
1547 /// Returns the list of directories that should be considered part of the
1548 /// local project.
1549 @protected
1550 @visibleForTesting
1551 Future<Map<String, dynamic>> pubRootDirectories(
1552 Map<String, String> parameters,
1553 ) {
1554 return Future<Map<String, Object>>.value(<String, Object>{
1555 'result': _pubRootDirectories ?? <String>[],
1556 });
1557 }
1558
1559 /// Set the [WidgetInspector] selection to the object matching the specified
1560 /// id if the object is valid object to set as the inspector selection.
1561 ///
1562 /// Returns true if the selection was changed.
1563 ///
1564 /// The `groupName` parameter is not required by is added to regularize the
1565 /// API surface of methods called from the Flutter IntelliJ Plugin.
1566 @protected
1567 bool setSelectionById(String? id, [ String? groupName ]) {
1568 return setSelection(toObject(id), groupName);
1569 }
1570
1571 /// Set the [WidgetInspector] selection to the specified `object` if it is
1572 /// a valid object to set as the inspector selection.
1573 ///
1574 /// Returns true if the selection was changed.
1575 ///
1576 /// The `groupName` parameter is not needed but is specified to regularize the
1577 /// API surface of methods called from the Flutter IntelliJ Plugin.
1578 @protected
1579 bool setSelection(Object? object, [ String? groupName ]) {
1580 if (object is Element || object is RenderObject) {
1581 if (object is Element) {
1582 if (object == selection.currentElement) {
1583 return false;
1584 }
1585 selection.currentElement = object;
1586 _sendInspectEvent(selection.currentElement);
1587 } else {
1588 if (object == selection.current) {
1589 return false;
1590 }
1591 selection.current = object! as RenderObject;
1592 _sendInspectEvent(selection.current);
1593 }
1594
1595 return true;
1596 }
1597 return false;
1598 }
1599
1600 /// Notify attached tools to navigate to an object's source location.
1601 void _sendInspectEvent(Object? object){
1602 inspect(object);
1603
1604 final _Location? location = _getSelectedSummaryWidgetLocation(null);
1605 if (location != null) {
1606 postEvent(
1607 'navigate',
1608 <String, Object>{
1609 'fileUri': location.file, // URI file path of the location.
1610 'line': location.line, // 1-based line number.
1611 'column': location.column, // 1-based column number.
1612 'source': 'flutter.inspector',
1613 },
1614 stream: 'ToolEvent',
1615 );
1616 }
1617 }
1618
1619 /// Returns a DevTools uri linking to a specific element on the inspector page.
1620 String? _devToolsInspectorUriForElement(Element element) {
1621 if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
1622 final String? inspectorRef = toId(element, _consoleObjectGroup);
1623 if (inspectorRef != null) {
1624 return devToolsInspectorUri(inspectorRef);
1625 }
1626 }
1627 return null;
1628 }
1629
1630 /// Returns the DevTools inspector uri for the given vm service connection and
1631 /// inspector reference.
1632 @visibleForTesting
1633 String devToolsInspectorUri(String inspectorRef) {
1634 assert(activeDevToolsServerAddress != null);
1635 assert(connectedVmServiceUri != null);
1636
1637 final Uri uri = Uri.parse(activeDevToolsServerAddress!).replace(
1638 queryParameters: <String, dynamic>{
1639 'uri': connectedVmServiceUri,
1640 'inspectorRef': inspectorRef,
1641 },
1642 );
1643
1644 // We cannot add the '/#/inspector' path by means of
1645 // [Uri.replace(path: '/#/inspector')] because the '#' character will be
1646 // encoded when we try to print the url as a string. DevTools will not
1647 // load properly if this character is encoded in the url.
1648 // Related: https://github.com/flutter/devtools/issues/2475.
1649 final String devToolsInspectorUri = uri.toString();
1650 final int startQueryParamIndex = devToolsInspectorUri.indexOf('?');
1651 // The query parameter character '?' should be present because we manually
1652 // added query parameters above.
1653 assert(startQueryParamIndex != -1);
1654 return '${devToolsInspectorUri.substring(0, startQueryParamIndex)}'
1655 '/#/inspector'
1656 '${devToolsInspectorUri.substring(startQueryParamIndex)}';
1657 }
1658
1659 /// Returns JSON representing the chain of [DiagnosticsNode] instances from
1660 /// root of the tree to the [Element] or [RenderObject] matching `id`.
1661 ///
1662 /// The JSON contains all information required to display a tree view with
1663 /// all nodes other than nodes along the path collapsed.
1664 @protected
1665 String getParentChain(String id, String groupName) {
1666 return _safeJsonEncode(_getParentChain(id, groupName));
1667 }
1668
1669 List<Object?> _getParentChain(String? id, String groupName) {
1670 final Object? value = toObject(id);
1671 List<_DiagnosticsPathNode> path;
1672 if (value is RenderObject) {
1673 path = _getRenderObjectParentChain(value, groupName)!;
1674 } else if (value is Element) {
1675 path = _getElementParentChain(value, groupName);
1676 } else {
1677 throw FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Cannot get parent chain for node of type ${value.runtimeType}')]);
1678 }
1679
1680 return path.map<Object?>((_DiagnosticsPathNode node) => _pathNodeToJson(
1681 node,
1682 InspectorSerializationDelegate(groupName: groupName, service: this),
1683 )).toList();
1684 }
1685
1686 Map<String, Object?>? _pathNodeToJson(_DiagnosticsPathNode? pathNode, InspectorSerializationDelegate delegate) {
1687 if (pathNode == null) {
1688 return null;
1689 }
1690 return <String, Object?>{
1691 'node': _nodeToJson(pathNode.node, delegate),
1692 'children': _nodesToJson(pathNode.children, delegate, parent: pathNode.node),
1693 'childIndex': pathNode.childIndex,
1694 };
1695 }
1696
1697 List<Element> _getRawElementParentChain(Element element, { required int? numLocalParents }) {
1698 List<Element> elements = element.debugGetDiagnosticChain();
1699 if (numLocalParents != null) {
1700 for (int i = 0; i < elements.length; i += 1) {
1701 if (_isValueCreatedByLocalProject(elements[i])) {
1702 numLocalParents = numLocalParents! - 1;
1703 if (numLocalParents <= 0) {
1704 elements = elements.take(i + 1).toList();
1705 break;
1706 }
1707 }
1708 }
1709 }
1710 return elements.reversed.toList();
1711 }
1712
1713 List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName, { int? numLocalParents }) {
1714 return _followDiagnosticableChain(
1715 _getRawElementParentChain(element, numLocalParents: numLocalParents),
1716 ) ?? const <_DiagnosticsPathNode>[];
1717 }
1718
1719 List<_DiagnosticsPathNode>? _getRenderObjectParentChain(RenderObject? renderObject, String groupName) {
1720 final List<RenderObject> chain = <RenderObject>[];
1721 while (renderObject != null) {
1722 chain.add(renderObject);
1723 renderObject = renderObject.parent;
1724 }
1725 return _followDiagnosticableChain(chain.reversed.toList());
1726 }
1727
1728 Map<String, Object?>? _nodeToJson(
1729 DiagnosticsNode? node,
1730 InspectorSerializationDelegate delegate,
1731 ) {
1732 return node?.toJsonMap(delegate);
1733 }
1734
1735 bool _isValueCreatedByLocalProject(Object? value) {
1736 final _Location? creationLocation = _getCreationLocation(value);
1737 if (creationLocation == null) {
1738 return false;
1739 }
1740 return _isLocalCreationLocation(creationLocation.file);
1741 }
1742
1743 bool _isLocalCreationLocationImpl(String locationUri) {
1744 final String file = Uri.parse(locationUri).path;
1745
1746 // By default check whether the creation location was within package:flutter.
1747 if (_pubRootDirectories == null) {
1748 // TODO(chunhtai): Make it more robust once
1749 // https://github.com/flutter/flutter/issues/32660 is fixed.
1750 return !file.contains('packages/flutter/');
1751 }
1752 for (final String directory in _pubRootDirectories!) {
1753 if (file.startsWith(directory)) {
1754 return true;
1755 }
1756 }
1757 return false;
1758 }
1759
1760 /// Memoized version of [_isLocalCreationLocationImpl].
1761 bool _isLocalCreationLocation(String locationUri) {
1762 final bool? cachedValue = _isLocalCreationCache[locationUri];
1763 if (cachedValue != null) {
1764 return cachedValue;
1765 }
1766 final bool result = _isLocalCreationLocationImpl(locationUri);
1767 _isLocalCreationCache[locationUri] = result;
1768 return result;
1769 }
1770
1771 /// Wrapper around `json.encode` that uses a ring of cached values to prevent
1772 /// the Dart garbage collector from collecting objects between when
1773 /// the value is returned over the VM service protocol and when the
1774 /// separate observatory protocol command has to be used to retrieve its full
1775 /// contents.
1776 //
1777 // TODO(jacobr): Replace this with a better solution once
1778 // https://github.com/dart-lang/sdk/issues/32919 is fixed.
1779 String _safeJsonEncode(Object? object) {
1780 final String jsonString = json.encode(object);
1781 _serializeRing[_serializeRingIndex] = jsonString;
1782 _serializeRingIndex = (_serializeRingIndex + 1) % _serializeRing.length;
1783 return jsonString;
1784 }
1785
1786 List<DiagnosticsNode> _truncateNodes(Iterable<DiagnosticsNode> nodes, int maxDescendentsTruncatableNode) {
1787 if (nodes.every((DiagnosticsNode node) => node.value is Element) && isWidgetCreationTracked()) {
1788 final List<DiagnosticsNode> localNodes = nodes
1789 .where((DiagnosticsNode node) => _isValueCreatedByLocalProject(node.value))
1790 .toList();
1791 if (localNodes.isNotEmpty) {
1792 return localNodes;
1793 }
1794 }
1795 return nodes.take(maxDescendentsTruncatableNode).toList();
1796 }
1797
1798 List<Map<String, Object?>> _nodesToJson(
1799 List<DiagnosticsNode> nodes,
1800 InspectorSerializationDelegate delegate, {
1801 required DiagnosticsNode? parent,
1802 }) {
1803 return DiagnosticsNode.toJsonList(nodes, parent, delegate);
1804 }
1805
1806 /// Returns a JSON representation of the properties of the [DiagnosticsNode]
1807 /// object that `diagnosticsNodeId` references.
1808 @protected
1809 String getProperties(String diagnosticsNodeId, String groupName) {
1810 return _safeJsonEncode(_getProperties(diagnosticsNodeId, groupName));
1811 }
1812
1813 List<Object> _getProperties(String? diagnosticableId, String groupName) {
1814 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1815 if (node == null) {
1816 return const <Object>[];
1817 }
1818 return _nodesToJson(node.getProperties(), InspectorSerializationDelegate(groupName: groupName, service: this), parent: node);
1819 }
1820
1821 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1822 /// object that `diagnosticsNodeId` references.
1823 String getChildren(String diagnosticsNodeId, String groupName) {
1824 return _safeJsonEncode(_getChildren(diagnosticsNodeId, groupName));
1825 }
1826
1827 List<Object> _getChildren(String? diagnosticsNodeId, String groupName) {
1828 final DiagnosticsNode? node = toObject(diagnosticsNodeId) as DiagnosticsNode?;
1829 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, service: this);
1830 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node);
1831 }
1832
1833 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1834 /// object that `diagnosticsNodeId` references only including children that
1835 /// were created directly by user code.
1836 ///
1837 /// {@template flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
1838 /// Requires [Widget] creation locations which are only available for debug
1839 /// mode builds when the `--track-widget-creation` flag is enabled on the call
1840 /// to the `flutter` tool. This flag is enabled by default in debug builds.
1841 /// {@endtemplate}
1842 ///
1843 /// See also:
1844 ///
1845 /// * [isWidgetCreationTracked] which indicates whether this method can be
1846 /// used.
1847 String getChildrenSummaryTree(String diagnosticsNodeId, String groupName) {
1848 return _safeJsonEncode(_getChildrenSummaryTree(diagnosticsNodeId, groupName));
1849 }
1850
1851 DiagnosticsNode? _idToDiagnosticsNode(String? diagnosticableId) {
1852 final Object? object = toObject(diagnosticableId);
1853 return objectToDiagnosticsNode(object);
1854 }
1855
1856 /// If possible, returns [DiagnosticsNode] for the object.
1857 @visibleForTesting
1858 static DiagnosticsNode? objectToDiagnosticsNode(Object? object) {
1859 if (object is Diagnosticable) {
1860 return object.toDiagnosticsNode();
1861 }
1862 return null;
1863 }
1864
1865 List<Object> _getChildrenSummaryTree(String? diagnosticableId, String groupName) {
1866 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1867 if (node == null) {
1868 return <Object>[];
1869 }
1870
1871 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, summaryTree: true, service: this);
1872 return _nodesToJson(_getChildrenFiltered(node, delegate), delegate, parent: node);
1873 }
1874
1875 /// Returns a JSON representation of the children of the [DiagnosticsNode]
1876 /// object that [diagnosticableId] references providing information needed
1877 /// for the details subtree view.
1878 ///
1879 /// The details subtree shows properties inline and includes all children
1880 /// rather than a filtered set of important children.
1881 String getChildrenDetailsSubtree(String diagnosticableId, String groupName) {
1882 return _safeJsonEncode(_getChildrenDetailsSubtree(diagnosticableId, groupName));
1883 }
1884
1885 List<Object> _getChildrenDetailsSubtree(String? diagnosticableId, String groupName) {
1886 final DiagnosticsNode? node = _idToDiagnosticsNode(diagnosticableId);
1887 // With this value of minDepth we only expand one extra level of important nodes.
1888 final InspectorSerializationDelegate delegate = InspectorSerializationDelegate(groupName: groupName, includeProperties: true, service: this);
1889 return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenFiltered(node, delegate), delegate, parent: node);
1890 }
1891
1892 bool _shouldShowInSummaryTree(DiagnosticsNode node) {
1893 if (node.level == DiagnosticLevel.error) {
1894 return true;
1895 }
1896 final Object? value = node.value;
1897 if (value is! Diagnosticable) {
1898 return true;
1899 }
1900 if (value is! Element || !isWidgetCreationTracked()) {
1901 // Creation locations are not available so include all nodes in the
1902 // summary tree.
1903 return true;
1904 }
1905 return _isValueCreatedByLocalProject(value);
1906 }
1907
1908 List<DiagnosticsNode> _getChildrenFiltered(
1909 DiagnosticsNode node,
1910 InspectorSerializationDelegate delegate,
1911 ) {
1912 return _filterChildren(node.getChildren(), delegate);
1913 }
1914
1915 List<DiagnosticsNode> _filterChildren(
1916 List<DiagnosticsNode> nodes,
1917 InspectorSerializationDelegate delegate,
1918 ) {
1919 final List<DiagnosticsNode> children = <DiagnosticsNode>[
1920 for (final DiagnosticsNode child in nodes)
1921 if (!delegate.summaryTree || _shouldShowInSummaryTree(child))
1922 child
1923 else
1924 ..._getChildrenFiltered(child, delegate),
1925 ];
1926 return children;
1927 }
1928
1929 /// Returns a JSON representation of the [DiagnosticsNode] for the root
1930 /// [Element].
1931 String getRootWidget(String groupName) {
1932 return _safeJsonEncode(_getRootWidget(groupName));
1933 }
1934
1935 Map<String, Object?>? _getRootWidget(String groupName) {
1936 return _nodeToJson(WidgetsBinding.instance.rootElement?.toDiagnosticsNode(), InspectorSerializationDelegate(groupName: groupName, service: this));
1937 }
1938
1939 /// Returns a JSON representation of the [DiagnosticsNode] for the root
1940 /// [Element] showing only nodes that should be included in a summary tree.
1941 String getRootWidgetSummaryTree(String groupName) {
1942 return _safeJsonEncode(_getRootWidgetSummaryTree(groupName));
1943 }
1944
1945 Map<String, Object?>? _getRootWidgetSummaryTree(
1946 String groupName, {
1947 Map<String, Object>? Function(DiagnosticsNode, InspectorSerializationDelegate)? addAdditionalPropertiesCallback,
1948 }) {
1949 return _nodeToJson(
1950 WidgetsBinding.instance.rootElement?.toDiagnosticsNode(),
1951 InspectorSerializationDelegate(
1952 groupName: groupName,
1953 subtreeDepth: 1000000,
1954 summaryTree: true,
1955 service: this,
1956 addAdditionalPropertiesCallback: addAdditionalPropertiesCallback,
1957 ),
1958 );
1959 }
1960
1961
1962 Future<Map<String, Object?>> _getRootWidgetSummaryTreeWithPreviews(
1963 Map<String, String> parameters,
1964 ) {
1965 final String groupName = parameters['groupName']!;
1966 final Map<String, Object?>? result = _getRootWidgetSummaryTree(
1967 groupName,
1968 addAdditionalPropertiesCallback: (DiagnosticsNode node, InspectorSerializationDelegate? delegate) {
1969 final Map<String, Object> additionalJson = <String, Object>{};
1970 final Object? value = node.value;
1971 if (value is Element) {
1972 final RenderObject? renderObject = value.renderObject;
1973 if (renderObject is RenderParagraph) {
1974 additionalJson['textPreview'] = renderObject.text.toPlainText();
1975 }
1976 }
1977 return additionalJson;
1978 },
1979 );
1980 return Future<Map<String, dynamic>>.value(<String, dynamic>{
1981 'result': result,
1982 });
1983 }
1984
1985 /// Returns a JSON representation of the subtree rooted at the
1986 /// [DiagnosticsNode] object that `diagnosticsNodeId` references providing
1987 /// information needed for the details subtree view.
1988 ///
1989 /// The number of levels of the subtree that should be returned is specified
1990 /// by the [subtreeDepth] parameter. This value defaults to 2 for backwards
1991 /// compatibility.
1992 ///
1993 /// See also:
1994 ///
1995 /// * [getChildrenDetailsSubtree], a method to get children of a node
1996 /// in the details subtree.
1997 String getDetailsSubtree(
1998 String diagnosticableId,
1999 String groupName, {
2000 int subtreeDepth = 2,
2001 }) {
2002 return _safeJsonEncode(_getDetailsSubtree(diagnosticableId, groupName, subtreeDepth));
2003 }
2004
2005 Map<String, Object?>? _getDetailsSubtree(
2006 String? diagnosticableId,
2007 String? groupName,
2008 int subtreeDepth,
2009 ) {
2010 final DiagnosticsNode? root = _idToDiagnosticsNode(diagnosticableId);
2011 if (root == null) {
2012 return null;
2013 }
2014 return _nodeToJson(
2015 root,
2016 InspectorSerializationDelegate(
2017 groupName: groupName,
2018 subtreeDepth: subtreeDepth,
2019 includeProperties: true,
2020 service: this,
2021 ),
2022 );
2023 }
2024
2025 /// Returns a [DiagnosticsNode] representing the currently selected [Element].
2026 @protected
2027 String getSelectedWidget(String? previousSelectionId, String groupName) {
2028 if (previousSelectionId != null) {
2029 debugPrint('previousSelectionId is deprecated in API');
2030 }
2031 return _safeJsonEncode(_getSelectedWidget(null, groupName));
2032 }
2033
2034 /// Captures an image of the current state of an [object] that is a
2035 /// [RenderObject] or [Element].
2036 ///
2037 /// The returned [ui.Image] has uncompressed raw RGBA bytes and will be scaled
2038 /// to be at most [width] pixels wide and [height] pixels tall. The returned
2039 /// image will never have a scale between logical pixels and the
2040 /// size of the output image larger than maxPixelRatio.
2041 /// [margin] indicates the number of pixels relative to the un-scaled size of
2042 /// the [object] to include as a margin to include around the bounds of the
2043 /// [object] in the screenshot. Including a margin can be useful to capture
2044 /// areas that are slightly outside of the normal bounds of an object such as
2045 /// some debug paint information.
2046 @protected
2047 Future<ui.Image?> screenshot(
2048 Object? object, {
2049 required double width,
2050 required double height,
2051 double margin = 0.0,
2052 double maxPixelRatio = 1.0,
2053 bool debugPaint = false,
2054 }) async {
2055 if (object is! Element && object is! RenderObject) {
2056 return null;
2057 }
2058 final RenderObject? renderObject = object is Element ? object.renderObject : (object as RenderObject?);
2059 if (renderObject == null || !renderObject.attached) {
2060 return null;
2061 }
2062
2063 if (renderObject.debugNeedsLayout) {
2064 final PipelineOwner owner = renderObject.owner!;
2065 assert(!owner.debugDoingLayout);
2066 owner
2067 ..flushLayout()
2068 ..flushCompositingBits()
2069 ..flushPaint();
2070
2071 // If we still need layout, then that means that renderObject was skipped
2072 // in the layout phase and therefore can't be painted. It is clearer to
2073 // return null indicating that a screenshot is unavailable than to return
2074 // an empty image.
2075 if (renderObject.debugNeedsLayout) {
2076 return null;
2077 }
2078 }
2079
2080 Rect renderBounds = _calculateSubtreeBounds(renderObject);
2081 if (margin != 0.0) {
2082 renderBounds = renderBounds.inflate(margin);
2083 }
2084 if (renderBounds.isEmpty) {
2085 return null;
2086 }
2087
2088 final double pixelRatio = math.min(
2089 maxPixelRatio,
2090 math.min(
2091 width / renderBounds.width,
2092 height / renderBounds.height,
2093 ),
2094 );
2095
2096 return _ScreenshotPaintingContext.toImage(
2097 renderObject,
2098 renderBounds,
2099 pixelRatio: pixelRatio,
2100 debugPaint: debugPaint,
2101 );
2102 }
2103
2104 Future<Map<String, Object?>> _getLayoutExplorerNode(
2105 Map<String, String> parameters,
2106 ) {
2107 final String? diagnosticableId = parameters['id'];
2108 final int subtreeDepth = int.parse(parameters['subtreeDepth']!);
2109 final String? groupName = parameters['groupName'];
2110 Map<String, dynamic>? result = <String, dynamic>{};
2111 final DiagnosticsNode? root = _idToDiagnosticsNode(diagnosticableId);
2112 if (root == null) {
2113 return Future<Map<String, dynamic>>.value(<String, dynamic>{
2114 'result': result,
2115 });
2116 }
2117 result = _nodeToJson(
2118 root,
2119 InspectorSerializationDelegate(
2120 groupName: groupName,
2121 summaryTree: true,
2122 subtreeDepth: subtreeDepth,
2123 service: this,
2124 addAdditionalPropertiesCallback:
2125 (DiagnosticsNode node, InspectorSerializationDelegate delegate) {
2126 final Object? value = node.value;
2127 final RenderObject? renderObject =
2128 value is Element ? value.renderObject : null;
2129 if (renderObject == null) {
2130 return const <String, Object>{};
2131 }
2132
2133 final DiagnosticsSerializationDelegate
2134 renderObjectSerializationDelegate = delegate.copyWith(
2135 subtreeDepth: 0,
2136 includeProperties: true,
2137 expandPropertyValues: false,
2138 );
2139 final Map<String, Object> additionalJson = <String, Object>{
2140 // Only include renderObject properties separately if this value is not already the renderObject.
2141 // Only include if we are expanding property values to mitigate the risk of infinite loops if
2142 // RenderObjects have properties that are Element objects.
2143 if (value is! RenderObject && delegate.expandPropertyValues)
2144 'renderObject': renderObject
2145 .toDiagnosticsNode()
2146 .toJsonMap(renderObjectSerializationDelegate),
2147 };
2148
2149 final RenderObject? renderParent = renderObject.parent;
2150 if (renderParent != null &&
2151 delegate.subtreeDepth > 0 &&
2152 delegate.expandPropertyValues) {
2153 final Object? parentCreator = renderParent.debugCreator;
2154 if (parentCreator is DebugCreator) {
2155 additionalJson['parentRenderElement'] =
2156 parentCreator.element.toDiagnosticsNode().toJsonMap(
2157 delegate.copyWith(
2158 subtreeDepth: 0,
2159 includeProperties: true,
2160 ),
2161 );
2162 // TODO(jacobr): also describe the path back up the tree to
2163 // the RenderParentElement from the current element. It
2164 // could be a surprising distance up the tree if a lot of
2165 // elements don't have their own RenderObjects.
2166 }
2167 }
2168
2169 try {
2170 if (!renderObject.debugNeedsLayout) {
2171 // ignore: invalid_use_of_protected_member
2172 final Constraints constraints = renderObject.constraints;
2173 final Map<String, Object> constraintsProperty = <String, Object>{
2174 'type': constraints.runtimeType.toString(),
2175 'description': constraints.toString(),
2176 };
2177 if (constraints is BoxConstraints) {
2178 constraintsProperty.addAll(<String, Object>{
2179 'minWidth': constraints.minWidth.toString(),
2180 'minHeight': constraints.minHeight.toString(),
2181 'maxWidth': constraints.maxWidth.toString(),
2182 'maxHeight': constraints.maxHeight.toString(),
2183 });
2184 }
2185 additionalJson['constraints'] = constraintsProperty;
2186 }
2187 } catch (e) {
2188 // Constraints are sometimes unavailable even though
2189 // debugNeedsLayout is false.
2190 }
2191
2192 try {
2193 if (renderObject is RenderBox) {
2194 additionalJson['isBox'] = true;
2195 additionalJson['size'] = <String, Object>{
2196 'width': renderObject.size.width.toString(),
2197 'height': renderObject.size.height.toString(),
2198 };
2199
2200 final ParentData? parentData = renderObject.parentData;
2201 if (parentData is FlexParentData) {
2202 additionalJson['flexFactor'] = parentData.flex!;
2203 additionalJson['flexFit'] =
2204 (parentData.fit ?? FlexFit.tight).name;
2205 } else if (parentData is BoxParentData) {
2206 final Offset offset = parentData.offset;
2207 additionalJson['parentData'] = <String, Object>{
2208 'offsetX': offset.dx.toString(),
2209 'offsetY': offset.dy.toString(),
2210 };
2211 }
2212 } else if (renderObject is RenderView) {
2213 additionalJson['size'] = <String, Object>{
2214 'width': renderObject.size.width.toString(),
2215 'height': renderObject.size.height.toString(),
2216 };
2217 }
2218 } catch (e) {
2219 // Not laid out yet.
2220 }
2221 return additionalJson;
2222 },
2223 ),
2224 );
2225 return Future<Map<String, dynamic>>.value(<String, dynamic>{
2226 'result': result,
2227 });
2228 }
2229
2230 Future<Map<String, dynamic>> _setFlexFit(Map<String, String> parameters) {
2231 final String? id = parameters['id'];
2232 final String parameter = parameters['flexFit']!;
2233 final FlexFit flexFit = _toEnumEntry<FlexFit>(FlexFit.values, parameter);
2234 final Object? object = toObject(id);
2235 bool succeed = false;
2236 if (object != null && object is Element) {
2237 final RenderObject? render = object.renderObject;
2238 final ParentData? parentData = render?.parentData;
2239 if (parentData is FlexParentData) {
2240 parentData.fit = flexFit;
2241 render!.markNeedsLayout();
2242 succeed = true;
2243 }
2244 }
2245 return Future<Map<String, Object>>.value(<String, Object>{
2246 'result': succeed,
2247 });
2248 }
2249
2250 Future<Map<String, dynamic>> _setFlexFactor(Map<String, String> parameters) {
2251 final String? id = parameters['id'];
2252 final String flexFactor = parameters['flexFactor']!;
2253 final int? factor = flexFactor == 'null' ? null : int.parse(flexFactor);
2254 final dynamic object = toObject(id);
2255 bool succeed = false;
2256 if (object != null && object is Element) {
2257 final RenderObject? render = object.renderObject;
2258 final ParentData? parentData = render?.parentData;
2259 if (parentData is FlexParentData) {
2260 parentData.flex = factor;
2261 render!.markNeedsLayout();
2262 succeed = true;
2263 }
2264 }
2265 return Future<Map<String, Object>>.value(<String, Object>{
2266 'result': succeed
2267 });
2268 }
2269
2270 Future<Map<String, dynamic>> _setFlexProperties(
2271 Map<String, String> parameters,
2272 ) {
2273 final String? id = parameters['id'];
2274 final MainAxisAlignment mainAxisAlignment = _toEnumEntry<MainAxisAlignment>(
2275 MainAxisAlignment.values,
2276 parameters['mainAxisAlignment']!,
2277 );
2278 final CrossAxisAlignment crossAxisAlignment =
2279 _toEnumEntry<CrossAxisAlignment>(
2280 CrossAxisAlignment.values,
2281 parameters['crossAxisAlignment']!,
2282 );
2283 final Object? object = toObject(id);
2284 bool succeed = false;
2285 if (object != null && object is Element) {
2286 final RenderObject? render = object.renderObject;
2287 if (render is RenderFlex) {
2288 render.mainAxisAlignment = mainAxisAlignment;
2289 render.crossAxisAlignment = crossAxisAlignment;
2290 render.markNeedsLayout();
2291 render.markNeedsPaint();
2292 succeed = true;
2293 }
2294 }
2295 return Future<Map<String, Object>>.value(<String, Object>{
2296 'result': succeed
2297 });
2298 }
2299
2300 T _toEnumEntry<T>(List<T> enumEntries, String name) {
2301 for (final T entry in enumEntries) {
2302 if (entry.toString() == name) {
2303 return entry;
2304 }
2305 }
2306 throw Exception('Enum value $name not found');
2307 }
2308
2309 Map<String, Object?>? _getSelectedWidget(String? previousSelectionId, String groupName) {
2310 return _nodeToJson(
2311 _getSelectedWidgetDiagnosticsNode(previousSelectionId),
2312 InspectorSerializationDelegate(groupName: groupName, service: this),
2313 );
2314 }
2315
2316 DiagnosticsNode? _getSelectedWidgetDiagnosticsNode(String? previousSelectionId) {
2317 final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
2318 final Element? current = selection.currentElement;
2319 return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
2320 }
2321
2322 /// Returns a [DiagnosticsNode] representing the currently selected [Element]
2323 /// if the selected [Element] should be shown in the summary tree otherwise
2324 /// returns the first ancestor of the selected [Element] shown in the summary
2325 /// tree.
2326 String getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2327 if (previousSelectionId != null) {
2328 debugPrint('previousSelectionId is deprecated in API');
2329 }
2330 return _safeJsonEncode(_getSelectedSummaryWidget(null, groupName));
2331 }
2332
2333 _Location? _getSelectedSummaryWidgetLocation(String? previousSelectionId) {
2334 return _getCreationLocation(_getSelectedSummaryDiagnosticsNode(previousSelectionId)?.value);
2335 }
2336
2337 DiagnosticsNode? _getSelectedSummaryDiagnosticsNode(String? previousSelectionId) {
2338 if (!isWidgetCreationTracked()) {
2339 return _getSelectedWidgetDiagnosticsNode(previousSelectionId);
2340 }
2341 final DiagnosticsNode? previousSelection = toObject(previousSelectionId) as DiagnosticsNode?;
2342 Element? current = selection.currentElement;
2343 if (current != null && !_isValueCreatedByLocalProject(current)) {
2344 Element? firstLocal;
2345 for (final Element candidate in current.debugGetDiagnosticChain()) {
2346 if (_isValueCreatedByLocalProject(candidate)) {
2347 firstLocal = candidate;
2348 break;
2349 }
2350 }
2351 current = firstLocal;
2352 }
2353 return current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode();
2354 }
2355
2356 Map<String, Object?>? _getSelectedSummaryWidget(String? previousSelectionId, String groupName) {
2357 return _nodeToJson(_getSelectedSummaryDiagnosticsNode(previousSelectionId), InspectorSerializationDelegate(groupName: groupName, service: this));
2358 }
2359
2360 /// Returns whether [Widget] creation locations are available.
2361 ///
2362 /// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
2363 bool isWidgetCreationTracked() {
2364 _widgetCreationTracked ??= const _WidgetForTypeTests() is _HasCreationLocation;
2365 return _widgetCreationTracked!;
2366 }
2367
2368 bool? _widgetCreationTracked;
2369
2370 late Duration _frameStart;
2371
2372 void _onFrameStart(Duration timeStamp) {
2373 _frameStart = timeStamp;
2374 SchedulerBinding.instance.addPostFrameCallback(_onFrameEnd, debugLabel: 'WidgetInspector.onFrameStart');
2375 }
2376
2377 void _onFrameEnd(Duration timeStamp) {
2378 if (_trackRebuildDirtyWidgets) {
2379 _postStatsEvent('Flutter.RebuiltWidgets', _rebuildStats);
2380 }
2381 if (_trackRepaintWidgets) {
2382 _postStatsEvent('Flutter.RepaintWidgets', _repaintStats);
2383 }
2384 }
2385
2386 void _postStatsEvent(String eventName, _ElementLocationStatsTracker stats) {
2387 postEvent(eventName, stats.exportToJson(_frameStart));
2388 }
2389
2390 /// All events dispatched by a [WidgetInspectorService] use this method
2391 /// instead of calling [developer.postEvent] directly.
2392 ///
2393 /// This allows tests for [WidgetInspectorService] to track which events were
2394 /// dispatched by overriding this method.
2395 @protected
2396 void postEvent(
2397 String eventKind,
2398 Map<Object, Object?> eventData, {
2399 String stream = 'Extension',
2400 }) {
2401 developer.postEvent(eventKind, eventData, stream: stream);
2402 }
2403
2404 /// All events dispatched by a [WidgetInspectorService] use this method
2405 /// instead of calling [developer.inspect].
2406 ///
2407 /// This allows tests for [WidgetInspectorService] to track which events were
2408 /// dispatched by overriding this method.
2409 @protected
2410 void inspect(Object? object) {
2411 developer.inspect(object);
2412 }
2413
2414 final _ElementLocationStatsTracker _rebuildStats = _ElementLocationStatsTracker();
2415 final _ElementLocationStatsTracker _repaintStats = _ElementLocationStatsTracker();
2416
2417 void _onRebuildWidget(Element element, bool builtOnce) {
2418 _rebuildStats.add(element);
2419 }
2420
2421 void _onPaint(RenderObject renderObject) {
2422 try {
2423 final Element? element = (renderObject.debugCreator as DebugCreator?)?.element;
2424 if (element is! RenderObjectElement) {
2425 // This branch should not hit as long as all RenderObjects were created
2426 // by Widgets. It is possible there might be some render objects
2427 // created directly without using the Widget layer so we add this check
2428 // to improve robustness.
2429 return;
2430 }
2431 _repaintStats.add(element);
2432
2433 // Give all ancestor elements credit for repainting as long as they do
2434 // not have their own associated RenderObject.
2435 element.visitAncestorElements((Element ancestor) {
2436 if (ancestor is RenderObjectElement) {
2437 // This ancestor has its own RenderObject so we can precisely track
2438 // when it repaints.
2439 return false;
2440 }
2441 _repaintStats.add(ancestor);
2442 return true;
2443 });
2444 }
2445 catch (exception, stack) {
2446 FlutterError.reportError(
2447 FlutterErrorDetails(
2448 exception: exception,
2449 stack: stack,
2450 library: 'widget inspector library',
2451 context: ErrorDescription('while tracking widget repaints'),
2452 ),
2453 );
2454 }
2455 }
2456
2457 /// This method is called by [WidgetsBinding.performReassemble] to flush caches
2458 /// of obsolete values after a hot reload.
2459 ///
2460 /// Do not call this method directly. Instead, use
2461 /// [BindingBase.reassembleApplication].
2462 void performReassemble() {
2463 _clearStats();
2464 _resetErrorCount();
2465 }
2466}
2467
2468/// Accumulator for a count associated with a specific source location.
2469///
2470/// The accumulator stores whether the source location is [local] and what its
2471/// [id] for efficiency encoding terse JSON payloads describing counts.
2472class _LocationCount {
2473 _LocationCount({
2474 required this.location,
2475 required this.id,
2476 required this.local,
2477 });
2478
2479 /// Location id.
2480 final int id;
2481
2482 /// Whether the location is local to the current project.
2483 final bool local;
2484
2485 final _Location location;
2486
2487 int get count => _count;
2488 int _count = 0;
2489
2490 /// Reset the count.
2491 void reset() {
2492 _count = 0;
2493 }
2494
2495 /// Increment the count.
2496 void increment() {
2497 _count++;
2498 }
2499}
2500
2501/// A stat tracker that aggregates a performance metric for [Element] objects at
2502/// the granularity of creation locations in source code.
2503///
2504/// This class is optimized to minimize the size of the JSON payloads describing
2505/// the aggregate statistics, for stable memory usage, and low CPU usage at the
2506/// expense of somewhat higher overall memory usage. Stable memory usage is more
2507/// important than peak memory usage to avoid the false impression that the
2508/// user's app is leaking memory each frame.
2509///
2510/// The number of unique widget creation locations tends to be at most in the
2511/// low thousands for regular flutter apps so the peak memory usage for this
2512/// class is not an issue.
2513class _ElementLocationStatsTracker {
2514 // All known creation location tracked.
2515 //
2516 // This could also be stored as a `Map` but this
2517 // representation is more efficient as all location ids from 0 to n are
2518 // typically present.
2519 //
2520 // All logic in this class assumes that if `_stats[i]` is not null
2521 // `_stats[i].id` equals `i`.
2522 final List<_LocationCount?> _stats = <_LocationCount?>[];
2523
2524 /// Locations with a non-zero count.
2525 final List<_LocationCount> active = <_LocationCount>[];
2526
2527 /// Locations that were added since stats were last exported.
2528 ///
2529 /// Only locations local to the current project are included as a performance
2530 /// optimization.
2531 final List<_LocationCount> newLocations = <_LocationCount>[];
2532
2533 /// Increments the count associated with the creation location of [element] if
2534 /// the creation location is local to the current project.
2535 void add(Element element) {
2536 final Object widget = element.widget;
2537 if (widget is! _HasCreationLocation) {
2538 return;
2539 }
2540 final _HasCreationLocation creationLocationSource = widget;
2541 final _Location? location = creationLocationSource._location;
2542 if (location == null) {
2543 return;
2544 }
2545 final int id = _toLocationId(location);
2546
2547 _LocationCount entry;
2548 if (id >= _stats.length || _stats[id] == null) {
2549 // After the first frame, almost all creation ids will already be in
2550 // _stats so this slow path will rarely be hit.
2551 while (id >= _stats.length) {
2552 _stats.add(null);
2553 }
2554 entry = _LocationCount(
2555 location: location,
2556 id: id,
2557 local: WidgetInspectorService.instance._isLocalCreationLocation(location.file),
2558 );
2559 if (entry.local) {
2560 newLocations.add(entry);
2561 }
2562 _stats[id] = entry;
2563 } else {
2564 entry = _stats[id]!;
2565 }
2566
2567 // We could in the future add an option to track stats for all widgets but
2568 // that would significantly increase the size of the events posted using
2569 // [developer.postEvent] and current use cases for this feature focus on
2570 // helping users find problems with their widgets not the platform
2571 // widgets.
2572 if (entry.local) {
2573 if (entry.count == 0) {
2574 active.add(entry);
2575 }
2576 entry.increment();
2577 }
2578 }
2579
2580 /// Clear all aggregated statistics.
2581 void resetCounts() {
2582 // We chose to only reset the active counts instead of clearing all data
2583 // to reduce the number memory allocations performed after the first frame.
2584 // Once an app has warmed up, location stats tracking should not
2585 // trigger significant additional memory allocations. Avoiding memory
2586 // allocations is important to minimize the impact this class has on cpu
2587 // and memory performance of the running app.
2588 for (final _LocationCount entry in active) {
2589 entry.reset();
2590 }
2591 active.clear();
2592 }
2593
2594 /// Exports the current counts and then resets the stats to prepare to track
2595 /// the next frame of data.
2596 Map<String, dynamic> exportToJson(Duration startTime) {
2597 final List<int> events = List<int>.filled(active.length * 2, 0);
2598 int j = 0;
2599 for (final _LocationCount stat in active) {
2600 events[j++] = stat.id;
2601 events[j++] = stat.count;
2602 }
2603
2604 final Map<String, dynamic> json = <String, dynamic>{
2605 'startTime': startTime.inMicroseconds,
2606 'events': events,
2607 };
2608
2609 // Encode the new locations using the older encoding.
2610 if (newLocations.isNotEmpty) {
2611 // Add all newly used location ids to the JSON.
2612 final Map<String, List<int>> locationsJson = <String, List<int>>{};
2613 for (final _LocationCount entry in newLocations) {
2614 final _Location location = entry.location;
2615 final List<int> jsonForFile = locationsJson.putIfAbsent(
2616 location.file,
2617 () => <int>[],
2618 );
2619 jsonForFile..add(entry.id)..add(location.line)..add(location.column);
2620 }
2621 json['newLocations'] = locationsJson;
2622 }
2623
2624 // Encode the new locations using the newer encoding (as of v2.4.0).
2625 if (newLocations.isNotEmpty) {
2626 final Map<String, Map<String, List<Object?>>> fileLocationsMap = <String, Map<String, List<Object?>>>{};
2627 for (final _LocationCount entry in newLocations) {
2628 final _Location location = entry.location;
2629 final Map<String, List<Object?>> locations = fileLocationsMap.putIfAbsent(
2630 location.file, () => <String, List<Object?>>{
2631 'ids': <int>[],
2632 'lines': <int>[],
2633 'columns': <int>[],
2634 'names': <String?>[],
2635 },
2636 );
2637
2638 locations['ids']!.add(entry.id);
2639 locations['lines']!.add(location.line);
2640 locations['columns']!.add(location.column);
2641 locations['names']!.add(location.name);
2642 }
2643 json['locations'] = fileLocationsMap;
2644 }
2645
2646 resetCounts();
2647 newLocations.clear();
2648 return json;
2649 }
2650}
2651
2652class _WidgetForTypeTests extends Widget {
2653 const _WidgetForTypeTests();
2654
2655 @override
2656 Element createElement() => throw UnimplementedError();
2657}
2658
2659/// A widget that enables inspecting the child widget's structure.
2660///
2661/// Select a location on your device or emulator and view what widgets and
2662/// render object that best matches the location. An outline of the selected
2663/// widget and terse summary information is shown on device with detailed
2664/// information is shown in the observatory or in IntelliJ when using the
2665/// Flutter Plugin.
2666///
2667/// The inspector has a select mode and a view mode.
2668///
2669/// In the select mode, tapping the device selects the widget that best matches
2670/// the location of the touch and switches to view mode. Dragging a finger on
2671/// the device selects the widget under the drag location but does not switch
2672/// modes. Touching the very edge of the bounding box of a widget triggers
2673/// selecting the widget even if another widget that also overlaps that
2674/// location would otherwise have priority.
2675///
2676/// In the view mode, the previously selected widget is outlined, however,
2677/// touching the device has the same effect it would have if the inspector
2678/// wasn't present. This allows interacting with the application and viewing how
2679/// the selected widget changes position. Clicking on the select icon in the
2680/// bottom left corner of the application switches back to select mode.
2681class WidgetInspector extends StatefulWidget {
2682 /// Creates a widget that enables inspection for the child.
2683 const WidgetInspector({
2684 super.key,
2685 required this.child,
2686 required this.selectButtonBuilder,
2687 });
2688
2689 /// The widget that is being inspected.
2690 final Widget child;
2691
2692 /// A builder that is called to create the select button.
2693 ///
2694 /// The `onPressed` callback passed as an argument to the builder should be
2695 /// hooked up to the returned widget.
2696 final InspectorSelectButtonBuilder? selectButtonBuilder;
2697
2698 @override
2699 State<WidgetInspector> createState() => _WidgetInspectorState();
2700}
2701
2702class _WidgetInspectorState extends State<WidgetInspector>
2703 with WidgetsBindingObserver {
2704
2705 _WidgetInspectorState();
2706
2707 Offset? _lastPointerLocation;
2708
2709 late InspectorSelection selection;
2710
2711 late bool isSelectMode;
2712
2713 final GlobalKey _ignorePointerKey = GlobalKey();
2714
2715 /// Distance from the edge of the bounding box for an element to consider
2716 /// as selecting the edge of the bounding box.
2717 static const double _edgeHitMargin = 2.0;
2718
2719 @override
2720 void initState() {
2721 super.initState();
2722
2723 WidgetInspectorService.instance.selection
2724 .addListener(_selectionInformationChanged);
2725 WidgetInspectorService.instance.isSelectMode
2726 .addListener(_selectionInformationChanged);
2727 selection = WidgetInspectorService.instance.selection;
2728 isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
2729 }
2730
2731 @override
2732 void dispose() {
2733 WidgetInspectorService.instance.selection
2734 .removeListener(_selectionInformationChanged);
2735 WidgetInspectorService.instance.isSelectMode
2736 .removeListener(_selectionInformationChanged);
2737 super.dispose();
2738 }
2739
2740 void _selectionInformationChanged() => setState((){
2741 selection = WidgetInspectorService.instance.selection;
2742 isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
2743 });
2744
2745 bool _hitTestHelper(
2746 List<RenderObject> hits,
2747 List<RenderObject> edgeHits,
2748 Offset position,
2749 RenderObject object,
2750 Matrix4 transform,
2751 ) {
2752 bool hit = false;
2753 final Matrix4? inverse = Matrix4.tryInvert(transform);
2754 if (inverse == null) {
2755 // We cannot invert the transform. That means the object doesn't appear on
2756 // screen and cannot be hit.
2757 return false;
2758 }
2759 final Offset localPosition = MatrixUtils.transformPoint(inverse, position);
2760
2761 final List<DiagnosticsNode> children = object.debugDescribeChildren();
2762 for (int i = children.length - 1; i >= 0; i -= 1) {
2763 final DiagnosticsNode diagnostics = children[i];
2764 if (diagnostics.style == DiagnosticsTreeStyle.offstage ||
2765 diagnostics.value is! RenderObject) {
2766 continue;
2767 }
2768 final RenderObject child = diagnostics.value! as RenderObject;
2769 final Rect? paintClip = object.describeApproximatePaintClip(child);
2770 if (paintClip != null && !paintClip.contains(localPosition)) {
2771 continue;
2772 }
2773
2774 final Matrix4 childTransform = transform.clone();
2775 object.applyPaintTransform(child, childTransform);
2776 if (_hitTestHelper(hits, edgeHits, position, child, childTransform)) {
2777 hit = true;
2778 }
2779 }
2780
2781 final Rect bounds = object.semanticBounds;
2782 if (bounds.contains(localPosition)) {
2783 hit = true;
2784 // Hits that occur on the edge of the bounding box of an object are
2785 // given priority to provide a way to select objects that would
2786 // otherwise be hard to select.
2787 if (!bounds.deflate(_edgeHitMargin).contains(localPosition)) {
2788 edgeHits.add(object);
2789 }
2790 }
2791 if (hit) {
2792 hits.add(object);
2793 }
2794 return hit;
2795 }
2796
2797 /// Returns the list of render objects located at the given position ordered
2798 /// by priority.
2799 ///
2800 /// All render objects that are not offstage that match the location are
2801 /// included in the list of matches. Priority is given to matches that occur
2802 /// on the edge of a render object's bounding box and to matches found by
2803 /// [RenderBox.hitTest].
2804 List<RenderObject> hitTest(Offset position, RenderObject root) {
2805 final List<RenderObject> regularHits = <RenderObject>[];
2806 final List<RenderObject> edgeHits = <RenderObject>[];
2807
2808 _hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null));
2809 // Order matches by the size of the hit area.
2810 double area(RenderObject object) {
2811 final Size size = object.semanticBounds.size;
2812 return size.width * size.height;
2813 }
2814 regularHits.sort((RenderObject a, RenderObject b) => area(a).compareTo(area(b)));
2815 final Set<RenderObject> hits = <RenderObject>{
2816 ...edgeHits,
2817 ...regularHits,
2818 };
2819 return hits.toList();
2820 }
2821
2822 void _inspectAt(Offset position) {
2823 if (!isSelectMode) {
2824 return;
2825 }
2826
2827 final RenderIgnorePointer ignorePointer = _ignorePointerKey.currentContext!.findRenderObject()! as RenderIgnorePointer;
2828 final RenderObject userRender = ignorePointer.child!;
2829 final List<RenderObject> selected = hitTest(position, userRender);
2830
2831 selection.candidates = selected;
2832 }
2833
2834 void _handlePanDown(DragDownDetails event) {
2835 _lastPointerLocation = event.globalPosition;
2836 _inspectAt(event.globalPosition);
2837 }
2838
2839 void _handlePanUpdate(DragUpdateDetails event) {
2840 _lastPointerLocation = event.globalPosition;
2841 _inspectAt(event.globalPosition);
2842 }
2843
2844 void _handlePanEnd(DragEndDetails details) {
2845 // If the pan ends on the edge of the window assume that it indicates the
2846 // pointer is being dragged off the edge of the display not a regular touch
2847 // on the edge of the display. If the pointer is being dragged off the edge
2848 // of the display we do not want to select anything. A user can still select
2849 // a widget that is only at the exact screen margin by tapping.
2850 final ui.FlutterView view = View.of(context);
2851 final Rect bounds = (Offset.zero & (view.physicalSize / view.devicePixelRatio)).deflate(_kOffScreenMargin);
2852 if (!bounds.contains(_lastPointerLocation!)) {
2853 selection.clear();
2854 }
2855 }
2856
2857 void _handleTap() {
2858 if (!isSelectMode) {
2859 return;
2860 }
2861 if (_lastPointerLocation != null) {
2862 _inspectAt(_lastPointerLocation!);
2863 WidgetInspectorService.instance._sendInspectEvent(selection.current);
2864 }
2865
2866 // Only exit select mode if there is a button to return to select mode.
2867 if (widget.selectButtonBuilder != null) {
2868 WidgetInspectorService.instance.isSelectMode.value = false;
2869 }
2870 }
2871
2872 void _handleEnableSelect() {
2873 WidgetInspectorService.instance.isSelectMode.value = true;
2874 }
2875
2876 @override
2877 Widget build(BuildContext context) {
2878 // Be careful changing this build method. The _InspectorOverlayLayer
2879 // assumes the root RenderObject for the WidgetInspector will be
2880 // a RenderStack with a _RenderInspectorOverlay as the last child.
2881 return Stack(children: <Widget>[
2882 GestureDetector(
2883 onTap: _handleTap,
2884 onPanDown: _handlePanDown,
2885 onPanEnd: _handlePanEnd,
2886 onPanUpdate: _handlePanUpdate,
2887 behavior: HitTestBehavior.opaque,
2888 excludeFromSemantics: true,
2889 child: IgnorePointer(
2890 ignoring: isSelectMode,
2891 key: _ignorePointerKey,
2892 child: widget.child,
2893 ),
2894 ),
2895 if (!isSelectMode && widget.selectButtonBuilder != null)
2896 Positioned(
2897 left: _kInspectButtonMargin,
2898 bottom: _kInspectButtonMargin,
2899 child: widget.selectButtonBuilder!(context, _handleEnableSelect),
2900 ),
2901 _InspectorOverlay(selection: selection),
2902 ]);
2903 }
2904}
2905
2906/// Mutable selection state of the inspector.
2907class InspectorSelection with ChangeNotifier {
2908 /// Creates an instance of [InspectorSelection].
2909 InspectorSelection() {
2910 if (kFlutterMemoryAllocationsEnabled) {
2911 ChangeNotifier.maybeDispatchObjectCreation(this);
2912 }
2913 }
2914
2915 /// Render objects that are candidates to be selected.
2916 ///
2917 /// Tools may wish to iterate through the list of candidates.
2918 List<RenderObject> get candidates => _candidates;
2919 List<RenderObject> _candidates = <RenderObject>[];
2920 set candidates(List<RenderObject> value) {
2921 _candidates = value;
2922 _index = 0;
2923 _computeCurrent();
2924 }
2925
2926 /// Index within the list of candidates that is currently selected.
2927 int get index => _index;
2928 int _index = 0;
2929 set index(int value) {
2930 _index = value;
2931 _computeCurrent();
2932 }
2933
2934 /// Set the selection to empty.
2935 void clear() {
2936 _candidates = <RenderObject>[];
2937 _index = 0;
2938 _computeCurrent();
2939 }
2940
2941 /// Selected render object typically from the [candidates] list.
2942 ///
2943 /// Setting [candidates] or calling [clear] resets the selection.
2944 ///
2945 /// Returns null if the selection is invalid.
2946 RenderObject? get current => active ? _current : null;
2947
2948 RenderObject? _current;
2949 set current(RenderObject? value) {
2950 if (_current != value) {
2951 _current = value;
2952 _currentElement = (value?.debugCreator as DebugCreator?)?.element;
2953 notifyListeners();
2954 }
2955 }
2956
2957 /// Selected [Element] consistent with the [current] selected [RenderObject].
2958 ///
2959 /// Setting [candidates] or calling [clear] resets the selection.
2960 ///
2961 /// Returns null if the selection is invalid.
2962 Element? get currentElement {
2963 return _currentElement?.debugIsDefunct ?? true ? null : _currentElement;
2964 }
2965
2966 Element? _currentElement;
2967 set currentElement(Element? element) {
2968 if (element?.debugIsDefunct ?? false) {
2969 _currentElement = null;
2970 _current = null;
2971 notifyListeners();
2972 return;
2973 }
2974 if (currentElement != element) {
2975 _currentElement = element;
2976 _current = element!.findRenderObject();
2977 notifyListeners();
2978 }
2979 }
2980
2981 void _computeCurrent() {
2982 if (_index < candidates.length) {
2983 _current = candidates[index];
2984 _currentElement = (_current?.debugCreator as DebugCreator?)?.element;
2985 notifyListeners();
2986 } else {
2987 _current = null;
2988 _currentElement = null;
2989 notifyListeners();
2990 }
2991 }
2992
2993 /// Whether the selected render object is attached to the tree or has gone
2994 /// out of scope.
2995 bool get active => _current != null && _current!.attached;
2996}
2997
2998class _InspectorOverlay extends LeafRenderObjectWidget {
2999 const _InspectorOverlay({
3000 required this.selection,
3001 });
3002
3003 final InspectorSelection selection;
3004
3005 @override
3006 _RenderInspectorOverlay createRenderObject(BuildContext context) {
3007 return _RenderInspectorOverlay(selection: selection);
3008 }
3009
3010 @override
3011 void updateRenderObject(BuildContext context, _RenderInspectorOverlay renderObject) {
3012 renderObject.selection = selection;
3013 }
3014}
3015
3016class _RenderInspectorOverlay extends RenderBox {
3017 _RenderInspectorOverlay({ required InspectorSelection selection })
3018 : _selection = selection;
3019
3020 InspectorSelection get selection => _selection;
3021 InspectorSelection _selection;
3022 set selection(InspectorSelection value) {
3023 if (value != _selection) {
3024 _selection = value;
3025 }
3026 markNeedsPaint();
3027 }
3028
3029 @override
3030 bool get sizedByParent => true;
3031
3032 @override
3033 bool get alwaysNeedsCompositing => true;
3034
3035 @override
3036 Size computeDryLayout(BoxConstraints constraints) {
3037 return constraints.constrain(Size.infinite);
3038 }
3039
3040 @override
3041 void paint(PaintingContext context, Offset offset) {
3042 assert(needsCompositing);
3043 context.addLayer(_InspectorOverlayLayer(
3044 overlayRect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
3045 selection: selection,
3046 rootRenderObject: parent is RenderObject ? parent! : null,
3047 ));
3048 }
3049}
3050
3051@immutable
3052class _TransformedRect {
3053 _TransformedRect(RenderObject object, RenderObject? ancestor)
3054 : rect = object.semanticBounds,
3055 transform = object.getTransformTo(ancestor);
3056
3057 final Rect rect;
3058 final Matrix4 transform;
3059
3060 @override
3061 bool operator ==(Object other) {
3062 if (other.runtimeType != runtimeType) {
3063 return false;
3064 }
3065 return other is _TransformedRect
3066 && other.rect == rect
3067 && other.transform == transform;
3068 }
3069
3070 @override
3071 int get hashCode => Object.hash(rect, transform);
3072}
3073
3074/// State describing how the inspector overlay should be rendered.
3075///
3076/// The equality operator can be used to determine whether the overlay needs to
3077/// be rendered again.
3078@immutable
3079class _InspectorOverlayRenderState {
3080 const _InspectorOverlayRenderState({
3081 required this.overlayRect,
3082 required this.selected,
3083 required this.candidates,
3084 required this.tooltip,
3085 required this.textDirection,
3086 });
3087
3088 final Rect overlayRect;
3089 final _TransformedRect selected;
3090 final List<_TransformedRect> candidates;
3091 final String tooltip;
3092 final TextDirection textDirection;
3093
3094 @override
3095 bool operator ==(Object other) {
3096 if (other.runtimeType != runtimeType) {
3097 return false;
3098 }
3099 return other is _InspectorOverlayRenderState
3100 && other.overlayRect == overlayRect
3101 && other.selected == selected
3102 && listEquals<_TransformedRect>(other.candidates, candidates)
3103 && other.tooltip == tooltip;
3104 }
3105
3106 @override
3107 int get hashCode => Object.hash(overlayRect, selected, Object.hashAll(candidates), tooltip);
3108}
3109
3110const int _kMaxTooltipLines = 5;
3111const Color _kTooltipBackgroundColor = Color.fromARGB(230, 60, 60, 60);
3112const Color _kHighlightedRenderObjectFillColor = Color.fromARGB(128, 128, 128, 255);
3113const Color _kHighlightedRenderObjectBorderColor = Color.fromARGB(128, 64, 64, 128);
3114
3115/// A layer that outlines the selected [RenderObject] and candidate render
3116/// objects that also match the last pointer location.
3117///
3118/// This approach is horrific for performance and is only used here because this
3119/// is limited to debug mode. Do not duplicate the logic in production code.
3120class _InspectorOverlayLayer extends Layer {
3121 /// Creates a layer that displays the inspector overlay.
3122 _InspectorOverlayLayer({
3123 required this.overlayRect,
3124 required this.selection,
3125 required this.rootRenderObject,
3126 }) {
3127 bool inDebugMode = false;
3128 assert(() {
3129 inDebugMode = true;
3130 return true;
3131 }());
3132 if (!inDebugMode) {
3133 throw FlutterError.fromParts(<DiagnosticsNode>[
3134 ErrorSummary(
3135 'The inspector should never be used in production mode due to the '
3136 'negative performance impact.',
3137 ),
3138 ]);
3139 }
3140 }
3141
3142 InspectorSelection selection;
3143
3144 /// The rectangle in this layer's coordinate system that the overlay should
3145 /// occupy.
3146 ///
3147 /// The scene must be explicitly recomposited after this property is changed
3148 /// (as described at [Layer]).
3149 final Rect overlayRect;
3150
3151 /// Widget inspector root render object. The selection overlay will be painted
3152 /// with transforms relative to this render object.
3153 final RenderObject? rootRenderObject;
3154
3155 _InspectorOverlayRenderState? _lastState;
3156
3157 /// Picture generated from _lastState.
3158 ui.Picture? _picture;
3159
3160 TextPainter? _textPainter;
3161 double? _textPainterMaxWidth;
3162
3163 @override
3164 void dispose() {
3165 _textPainter?.dispose();
3166 _textPainter = null;
3167 _picture?.dispose();
3168 super.dispose();
3169 }
3170
3171 @override
3172 void addToScene(ui.SceneBuilder builder) {
3173 if (!selection.active) {
3174 return;
3175 }
3176
3177 final RenderObject selected = selection.current!;
3178
3179 if (!_isInInspectorRenderObjectTree(selected)) {
3180 return;
3181 }
3182
3183 final List<_TransformedRect> candidates = <_TransformedRect>[];
3184 for (final RenderObject candidate in selection.candidates) {
3185 if (candidate == selected || !candidate.attached
3186 || !_isInInspectorRenderObjectTree(candidate)) {
3187 continue;
3188 }
3189 candidates.add(_TransformedRect(candidate, rootRenderObject));
3190 }
3191 final _TransformedRect selectedRect = _TransformedRect(selected, rootRenderObject);
3192 final String widgetName = selection.currentElement!.toStringShort();
3193 final String width = selectedRect.rect.width.toStringAsFixed(1);
3194 final String height = selectedRect.rect.height.toStringAsFixed(1);
3195
3196 final _InspectorOverlayRenderState state = _InspectorOverlayRenderState(
3197 overlayRect: overlayRect,
3198 selected: selectedRect,
3199 tooltip: '$widgetName ($width x $height)',
3200 textDirection: TextDirection.ltr,
3201 candidates: candidates,
3202 );
3203
3204 if (state != _lastState) {
3205 _lastState = state;
3206 _picture?.dispose();
3207 _picture = _buildPicture(state);
3208 }
3209 builder.addPicture(Offset.zero, _picture!);
3210 }
3211
3212 ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
3213 final ui.PictureRecorder recorder = ui.PictureRecorder();
3214 final Canvas canvas = Canvas(recorder, state.overlayRect);
3215 final Size size = state.overlayRect.size;
3216 // The overlay rect could have an offset if the widget inspector does
3217 // not take all the screen.
3218 canvas.translate(state.overlayRect.left, state.overlayRect.top);
3219
3220 final Paint fillPaint = Paint()
3221 ..style = PaintingStyle.fill
3222 ..color = _kHighlightedRenderObjectFillColor;
3223
3224 final Paint borderPaint = Paint()
3225 ..style = PaintingStyle.stroke
3226 ..strokeWidth = 1.0
3227 ..color = _kHighlightedRenderObjectBorderColor;
3228
3229 // Highlight the selected renderObject.
3230 final Rect selectedPaintRect = state.selected.rect.deflate(0.5);
3231 canvas
3232 ..save()
3233 ..transform(state.selected.transform.storage)
3234 ..drawRect(selectedPaintRect, fillPaint)
3235 ..drawRect(selectedPaintRect, borderPaint)
3236 ..restore();
3237
3238 // Show all other candidate possibly selected elements. This helps selecting
3239 // render objects by selecting the edge of the bounding box shows all
3240 // elements the user could toggle the selection between.
3241 for (final _TransformedRect transformedRect in state.candidates) {
3242 canvas
3243 ..save()
3244 ..transform(transformedRect.transform.storage)
3245 ..drawRect(transformedRect.rect.deflate(0.5), borderPaint)
3246 ..restore();
3247 }
3248
3249 final Rect targetRect = MatrixUtils.transformRect(
3250 state.selected.transform, state.selected.rect,
3251 );
3252 final Offset target = Offset(targetRect.left, targetRect.center.dy);
3253 const double offsetFromWidget = 9.0;
3254 final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
3255
3256 _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect);
3257
3258 // TODO(jacobr): provide an option to perform a debug paint of just the
3259 // selected widget.
3260 return recorder.endRecording();
3261 }
3262
3263 void _paintDescription(
3264 Canvas canvas,
3265 String message,
3266 TextDirection textDirection,
3267 Offset target,
3268 double verticalOffset,
3269 Size size,
3270 Rect targetRect,
3271 ) {
3272 canvas.save();
3273 final double maxWidth = math.max(
3274 size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding),
3275 0,
3276 );
3277 final TextSpan? textSpan = _textPainter?.text as TextSpan?;
3278 if (_textPainter == null || textSpan!.text != message || _textPainterMaxWidth != maxWidth) {
3279 _textPainterMaxWidth = maxWidth;
3280 _textPainter?.dispose();
3281 _textPainter = TextPainter()
3282 ..maxLines = _kMaxTooltipLines
3283 ..ellipsis = '...'
3284 ..text = TextSpan(style: _messageStyle, text: message)
3285 ..textDirection = textDirection
3286 ..layout(maxWidth: maxWidth);
3287 }
3288
3289 final Size tooltipSize = _textPainter!.size + const Offset(_kTooltipPadding * 2, _kTooltipPadding * 2);
3290 final Offset tipOffset = positionDependentBox(
3291 size: size,
3292 childSize: tooltipSize,
3293 target: target,
3294 verticalOffset: verticalOffset,
3295 preferBelow: false,
3296 );
3297
3298 final Paint tooltipBackground = Paint()
3299 ..style = PaintingStyle.fill
3300 ..color = _kTooltipBackgroundColor;
3301 canvas.drawRect(
3302 Rect.fromPoints(
3303 tipOffset,
3304 tipOffset.translate(tooltipSize.width, tooltipSize.height),
3305 ),
3306 tooltipBackground,
3307 );
3308
3309 double wedgeY = tipOffset.dy;
3310 final bool tooltipBelow = tipOffset.dy > target.dy;
3311 if (!tooltipBelow) {
3312 wedgeY += tooltipSize.height;
3313 }
3314
3315 const double wedgeSize = _kTooltipPadding * 2;
3316 double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2;
3317 wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2);
3318 final List<Offset> wedge = <Offset>[
3319 Offset(wedgeX - wedgeSize, wedgeY),
3320 Offset(wedgeX + wedgeSize, wedgeY),
3321 Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)),
3322 ];
3323 canvas.drawPath(Path()..addPolygon(wedge, true), tooltipBackground);
3324 _textPainter!.paint(canvas, tipOffset + const Offset(_kTooltipPadding, _kTooltipPadding));
3325 canvas.restore();
3326 }
3327
3328 @override
3329 @protected
3330 bool findAnnotations<S extends Object>(
3331 AnnotationResult<S> result,
3332 Offset localPosition, {
3333 bool? onlyFirst,
3334 }) {
3335 return false;
3336 }
3337
3338 /// Return whether or not a render object belongs to this inspector widget
3339 /// tree.
3340 /// The inspector selection is static, so if there are multiple inspector
3341 /// overlays in the same app (i.e. an storyboard), a selected or candidate
3342 /// render object may not belong to this tree.
3343 bool _isInInspectorRenderObjectTree(RenderObject child) {
3344 RenderObject? current = child.parent;
3345 while (current != null) {
3346 // We found the widget inspector render object.
3347 if (current is RenderStack
3348 && current.lastChild is _RenderInspectorOverlay) {
3349 return rootRenderObject == current;
3350 }
3351 current = current.parent;
3352 }
3353 return false;
3354 }
3355}
3356
3357const double _kScreenEdgeMargin = 10.0;
3358const double _kTooltipPadding = 5.0;
3359const double _kInspectButtonMargin = 10.0;
3360
3361/// Interpret pointer up events within with this margin as indicating the
3362/// pointer is moving off the device.
3363const double _kOffScreenMargin = 1.0;
3364
3365const TextStyle _messageStyle = TextStyle(
3366 color: Color(0xFFFFFFFF),
3367 fontSize: 10.0,
3368 height: 1.2,
3369);
3370
3371/// Interface for classes that track the source code location the their
3372/// constructor was called from.
3373///
3374/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3375// ignore: unused_element
3376abstract class _HasCreationLocation {
3377 _Location? get _location;
3378}
3379
3380/// A tuple with file, line, and column number, for displaying human-readable
3381/// file locations.
3382class _Location {
3383 const _Location({
3384 required this.file,
3385 required this.line,
3386 required this.column,
3387 // ignore: unused_element
3388 this.name,
3389 });
3390
3391 /// File path of the location.
3392 final String file;
3393
3394 /// 1-based line number.
3395 final int line;
3396
3397 /// 1-based column number.
3398 final int column;
3399
3400 /// Optional name of the parameter or function at this location.
3401 final String? name;
3402
3403 Map<String, Object?> toJsonMap() {
3404 final Map<String, Object?> json = <String, Object?>{
3405 'file': file,
3406 'line': line,
3407 'column': column,
3408 };
3409 if (name != null) {
3410 json['name'] = name;
3411 }
3412 return json;
3413 }
3414
3415 @override
3416 String toString() {
3417 final List<String> parts = <String>[];
3418 if (name != null) {
3419 parts.add(name!);
3420 }
3421 parts.add(file);
3422 parts..add('$line')..add('$column');
3423 return parts.join(':');
3424 }
3425}
3426
3427bool _isDebugCreator(DiagnosticsNode node) => node is DiagnosticsDebugCreator;
3428
3429/// Transformer to parse and gather information about [DiagnosticsDebugCreator].
3430///
3431/// This function will be registered to [FlutterErrorDetails.propertiesTransformers]
3432/// in [WidgetsBinding.initInstances].
3433///
3434/// This is meant to be called only in debug mode. In other modes, it yields an empty list.
3435Iterable<DiagnosticsNode> debugTransformDebugCreator(Iterable<DiagnosticsNode> properties) {
3436 if (!kDebugMode) {
3437 return <DiagnosticsNode>[];
3438 }
3439 final List<DiagnosticsNode> pending = <DiagnosticsNode>[];
3440 ErrorSummary? errorSummary;
3441 for (final DiagnosticsNode node in properties) {
3442 if (node is ErrorSummary) {
3443 errorSummary = node;
3444 break;
3445 }
3446 }
3447 bool foundStackTrace = false;
3448 final List<DiagnosticsNode> result = <DiagnosticsNode>[];
3449 for (final DiagnosticsNode node in properties) {
3450 if (!foundStackTrace && node is DiagnosticsStackTrace) {
3451 foundStackTrace = true;
3452 }
3453 if (_isDebugCreator(node)) {
3454 result.addAll(_parseDiagnosticsNode(node, errorSummary));
3455 } else {
3456 if (foundStackTrace) {
3457 pending.add(node);
3458 } else {
3459 result.add(node);
3460 }
3461 }
3462 }
3463 result.addAll(pending);
3464 return result;
3465}
3466
3467/// Transform the input [DiagnosticsNode].
3468///
3469/// Return null if input [DiagnosticsNode] is not applicable.
3470Iterable<DiagnosticsNode> _parseDiagnosticsNode(
3471 DiagnosticsNode node,
3472 ErrorSummary? errorSummary,
3473) {
3474 assert(_isDebugCreator(node));
3475 try {
3476 final DebugCreator debugCreator = node.value! as DebugCreator;
3477 final Element element = debugCreator.element;
3478 return _describeRelevantUserCode(element, errorSummary);
3479 } catch (error, stack) {
3480 scheduleMicrotask(() {
3481 FlutterError.reportError(FlutterErrorDetails(
3482 exception: error,
3483 stack: stack,
3484 library: 'widget inspector',
3485 informationCollector: () => <DiagnosticsNode>[
3486 DiagnosticsNode.message('This exception was caught while trying to describe the user-relevant code of another error.'),
3487 ],
3488 ));
3489 });
3490 return <DiagnosticsNode>[];
3491 }
3492}
3493
3494Iterable<DiagnosticsNode> _describeRelevantUserCode(
3495 Element element,
3496 ErrorSummary? errorSummary,
3497) {
3498 if (!WidgetInspectorService.instance.isWidgetCreationTracked()) {
3499 return <DiagnosticsNode>[
3500 ErrorDescription(
3501 'Widget creation tracking is currently disabled. Enabling '
3502 'it enables improved error messages. It can be enabled by passing '
3503 '`--track-widget-creation` to `flutter run` or `flutter test`.',
3504 ),
3505 ErrorSpacer(),
3506 ];
3507 }
3508
3509 bool isOverflowError() {
3510 if (errorSummary != null && errorSummary.value.isNotEmpty) {
3511 final Object summary = errorSummary.value.first;
3512 if (summary is String && summary.startsWith('A RenderFlex overflowed by')) {
3513 return true;
3514 }
3515 }
3516 return false;
3517 }
3518
3519 final List<DiagnosticsNode> nodes = <DiagnosticsNode>[];
3520 bool processElement(Element target) {
3521 // TODO(chunhtai): should print out all the widgets that are about to cross
3522 // package boundaries.
3523 if (debugIsLocalCreationLocation(target)) {
3524 DiagnosticsNode? devToolsDiagnostic;
3525
3526 // TODO(kenz): once the inspector is better at dealing with broken trees,
3527 // we can enable deep links for more errors than just RenderFlex overflow
3528 // errors. See https://github.com/flutter/flutter/issues/74918.
3529 if (isOverflowError()) {
3530 final String? devToolsInspectorUri =
3531 WidgetInspectorService.instance._devToolsInspectorUriForElement(target);
3532 if (devToolsInspectorUri != null) {
3533 devToolsDiagnostic = DevToolsDeepLinkProperty(
3534 'To inspect this widget in Flutter DevTools, visit: $devToolsInspectorUri',
3535 devToolsInspectorUri,
3536 );
3537 }
3538 }
3539
3540 nodes.addAll(<DiagnosticsNode>[
3541 DiagnosticsBlock(
3542 name: 'The relevant error-causing widget was',
3543 children: <DiagnosticsNode>[
3544 ErrorDescription('${target.widget.toStringShort()} ${_describeCreationLocation(target)}'),
3545 ],
3546 ),
3547 ErrorSpacer(),
3548 if (devToolsDiagnostic != null) ...<DiagnosticsNode>[devToolsDiagnostic, ErrorSpacer()],
3549 ]);
3550 return false;
3551 }
3552 return true;
3553 }
3554 if (processElement(element)) {
3555 element.visitAncestorElements(processElement);
3556 }
3557 return nodes;
3558}
3559
3560/// Debugging message for DevTools deep links.
3561///
3562/// The [value] for this property is a string representation of the Flutter
3563/// DevTools url.
3564class DevToolsDeepLinkProperty extends DiagnosticsProperty<String> {
3565 /// Creates a diagnostics property that displays a deep link to Flutter DevTools.
3566 ///
3567 /// The [value] of this property will return a map of data for the Flutter
3568 /// DevTools deep link, including the full `url`, the Flutter DevTools `screenId`,
3569 /// and the `objectId` in Flutter DevTools that this diagnostic references.
3570 DevToolsDeepLinkProperty(String description, String url)
3571 : super('', url, description: description, level: DiagnosticLevel.info);
3572}
3573
3574/// Returns if an object is user created.
3575///
3576/// This always returns false if it is not called in debug mode.
3577///
3578/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3579///
3580/// Currently is local creation locations are only available for
3581/// [Widget] and [Element].
3582bool debugIsLocalCreationLocation(Object object) {
3583 bool isLocal = false;
3584 assert(() {
3585 final _Location? location = _getCreationLocation(object);
3586 if (location != null) {
3587 isLocal = WidgetInspectorService.instance._isLocalCreationLocation(location.file);
3588 }
3589 return true;
3590 }());
3591 return isLocal;
3592}
3593
3594/// Returns true if a [Widget] is user created.
3595///
3596/// This is a faster variant of `debugIsLocalCreationLocation` that is available
3597/// in debug and profile builds but only works for [Widget].
3598bool debugIsWidgetLocalCreation(Widget widget) {
3599 final _Location? location = _getObjectCreationLocation(widget);
3600 return location != null &&
3601 WidgetInspectorService.instance._isLocalCreationLocation(location.file);
3602}
3603
3604/// Returns the creation location of an object in String format if one is available.
3605///
3606/// ex: "file:///path/to/main.dart:4:3"
3607///
3608/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3609///
3610/// Currently creation locations are only available for [Widget] and [Element].
3611String? _describeCreationLocation(Object object) {
3612 final _Location? location = _getCreationLocation(object);
3613 return location?.toString();
3614}
3615
3616_Location? _getObjectCreationLocation(Object object) {
3617 return object is _HasCreationLocation ? object._location : null;
3618}
3619
3620/// Returns the creation location of an object if one is available.
3621///
3622/// {@macro flutter.widgets.WidgetInspectorService.getChildrenSummaryTree}
3623///
3624/// Currently creation locations are only available for [Widget] and [Element].
3625_Location? _getCreationLocation(Object? object) {
3626 final Object? candidate = object is Element && !object.debugIsDefunct ? object.widget : object;
3627 return candidate == null ? null : _getObjectCreationLocation(candidate);
3628}
3629
3630// _Location objects are always const so we don't need to worry about the GC
3631// issues that are a concern for other object ids tracked by
3632// [WidgetInspectorService].
3633final Map<_Location, int> _locationToId = <_Location, int>{};
3634final List<_Location> _locations = <_Location>[];
3635
3636int _toLocationId(_Location location) {
3637 int? id = _locationToId[location];
3638 if (id != null) {
3639 return id;
3640 }
3641 id = _locations.length;
3642 _locations.add(location);
3643 _locationToId[location] = id;
3644 return id;
3645}
3646
3647/// A delegate that configures how a hierarchy of [DiagnosticsNode]s are
3648/// serialized by the Flutter Inspector.
3649@visibleForTesting
3650class InspectorSerializationDelegate implements DiagnosticsSerializationDelegate {
3651 /// Creates an [InspectorSerializationDelegate] that serialize [DiagnosticsNode]
3652 /// for Flutter Inspector service.
3653 InspectorSerializationDelegate({
3654 this.groupName,
3655 this.summaryTree = false,
3656 this.maxDescendantsTruncatableNode = -1,
3657 this.expandPropertyValues = true,
3658 this.subtreeDepth = 1,
3659 this.includeProperties = false,
3660 required this.service,
3661 this.addAdditionalPropertiesCallback,
3662 });
3663
3664 /// Service used by GUI tools to interact with the [WidgetInspector].
3665 final WidgetInspectorService service;
3666
3667 /// Optional [groupName] parameter which indicates that the json should
3668 /// contain live object ids.
3669 ///
3670 /// Object ids returned as part of the json will remain live at least until
3671 /// [WidgetInspectorService.disposeGroup()] is called on [groupName].
3672 final String? groupName;
3673
3674 /// Whether the tree should only include nodes created by the local project.
3675 final bool summaryTree;
3676
3677 /// Maximum descendants of [DiagnosticsNode] before truncating.
3678 final int maxDescendantsTruncatableNode;
3679
3680 @override
3681 final bool includeProperties;
3682
3683 @override
3684 final int subtreeDepth;
3685
3686 @override
3687 final bool expandPropertyValues;
3688
3689 /// Callback to add additional experimental serialization properties.
3690 ///
3691 /// This callback can be used to customize the serialization of DiagnosticsNode
3692 /// objects for experimental features in widget inspector clients such as
3693 /// [Dart DevTools](https://github.com/flutter/devtools).
3694 final Map<String, Object>? Function(DiagnosticsNode, InspectorSerializationDelegate)? addAdditionalPropertiesCallback;
3695
3696 final List<DiagnosticsNode> _nodesCreatedByLocalProject = <DiagnosticsNode>[];
3697
3698 bool get _interactive => groupName != null;
3699
3700 @override
3701 Map<String, Object?> additionalNodeProperties(DiagnosticsNode node) {
3702 final Map<String, Object?> result = <String, Object?>{};
3703 final Object? value = node.value;
3704 if (_interactive) {
3705 result['valueId'] = service.toId(value, groupName!);
3706 }
3707 if (summaryTree) {
3708 result['summaryTree'] = true;
3709 }
3710 final _Location? creationLocation = _getCreationLocation(value);
3711 if (creationLocation != null) {
3712 result['locationId'] = _toLocationId(creationLocation);
3713 result['creationLocation'] = creationLocation.toJsonMap();
3714 if (service._isLocalCreationLocation(creationLocation.file)) {
3715 _nodesCreatedByLocalProject.add(node);
3716 result['createdByLocalProject'] = true;
3717 }
3718 }
3719 if (addAdditionalPropertiesCallback != null) {
3720 result.addAll(addAdditionalPropertiesCallback!(node, this) ?? <String, Object>{});
3721 }
3722 return result;
3723 }
3724
3725 @override
3726 DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) {
3727 // The tricky special case here is that when in the detailsTree,
3728 // we keep subtreeDepth from going down to zero until we reach nodes
3729 // that also exist in the summary tree. This ensures that every time
3730 // you expand a node in the details tree, you expand the entire subtree
3731 // up until you reach the next nodes shared with the summary tree.
3732 return summaryTree || subtreeDepth > 1 || service._shouldShowInSummaryTree(node)
3733 ? copyWith(subtreeDepth: subtreeDepth - 1)
3734 : this;
3735 }
3736
3737 @override
3738 List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
3739 return service._filterChildren(nodes, this);
3740 }
3741
3742 @override
3743 List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
3744 final bool createdByLocalProject = _nodesCreatedByLocalProject.contains(owner);
3745 return nodes.where((DiagnosticsNode node) {
3746 return !node.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info);
3747 }).toList();
3748 }
3749
3750 @override
3751 List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
3752 if (maxDescendantsTruncatableNode >= 0 &&
3753 owner!.allowTruncate &&
3754 nodes.length > maxDescendantsTruncatableNode) {
3755 nodes = service._truncateNodes(nodes, maxDescendantsTruncatableNode);
3756 }
3757 return nodes;
3758 }
3759
3760 @override
3761 DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties, bool? expandPropertyValues}) {
3762 return InspectorSerializationDelegate(
3763 groupName: groupName,
3764 summaryTree: summaryTree,
3765 maxDescendantsTruncatableNode: maxDescendantsTruncatableNode,
3766 expandPropertyValues: expandPropertyValues ?? this.expandPropertyValues,
3767 subtreeDepth: subtreeDepth ?? this.subtreeDepth,
3768 includeProperties: includeProperties ?? this.includeProperties,
3769 service: service,
3770 addAdditionalPropertiesCallback: addAdditionalPropertiesCallback,
3771 );
3772 }
3773}
3774
3775@Target(<TargetKind>{TargetKind.method})
3776class _WidgetFactory {
3777 const _WidgetFactory();
3778}
3779
3780/// Annotation which marks a function as a widget factory for the purpose of
3781/// widget creation tracking.
3782///
3783/// When widget creation tracking is enabled, the framework tracks the source
3784/// code location of the constructor call for each widget instance. This
3785/// information is used by the DevTools to provide an improved developer
3786/// experience. For example, it allows the Flutter inspector to present the
3787/// widget tree in a manner similar to how the UI was defined in your source
3788/// code.
3789///
3790/// [Widget] constructors are automatically instrumented to track the source
3791/// code location of constructor calls. However, there are cases where
3792/// a function acts as a sort of a constructor for a widget and a call to such
3793/// a function should be considered as the creation location for the returned
3794/// widget instance.
3795///
3796/// Annotating a function with this annotation marks the function as a widget
3797/// factory. The framework will then instrument that function in the same way
3798/// as it does for [Widget] constructors.
3799///
3800/// Tracking will not work correctly if the function has optional positional
3801/// parameters.
3802///
3803/// Currently this annotation is only supported on extension methods.
3804///
3805/// {@tool snippet}
3806///
3807/// This example shows how to use the [widgetFactory] annotation to mark an
3808/// extension method as a widget factory:
3809///
3810/// ```dart
3811/// extension PaddingModifier on Widget {
3812/// @widgetFactory
3813/// Widget padding(EdgeInsetsGeometry padding) {
3814/// return Padding(padding: padding, child: this);
3815/// }
3816/// }
3817/// ```
3818///
3819/// When using the above extension method, the framework will track the
3820/// creation location of the [Padding] widget instance as the source code
3821/// location where the `padding` extension method was called:
3822///
3823/// ```dart
3824/// // continuing from previous example...
3825/// const Text('Hello World!')
3826/// .padding(const EdgeInsets.all(8));
3827/// ```
3828///
3829/// {@end-tool}
3830///
3831/// See also:
3832///
3833/// * the documentation for [Track widget creation](https://docs.flutter.dev/development/tools/devtools/inspector#track-widget-creation).
3834// The below ignore is needed because the static type of the annotation is used
3835// by the CFE kernel transformer that implements the instrumentation to
3836// recognize the annotation.
3837// ignore: library_private_types_in_public_api
3838const _WidgetFactory widgetFactory = _WidgetFactory();
3839
3840/// Does not hold keys from garbage collection.
3841@visibleForTesting
3842class WeakMap<K, V> {
3843 Expando<Object> _objects = Expando<Object>();
3844
3845 /// Strings, numbers, booleans.
3846 final Map<K, V> _primitives = <K, V>{};
3847
3848 bool _isPrimitive(Object? key) {
3849 return key == null || key is String || key is num || key is bool;
3850 }
3851
3852 /// Returns the value for the given [key] or null if [key] is not in the map
3853 /// or garbage collected.
3854 ///
3855 /// Does not support records to act as keys.
3856 V? operator [](K key){
3857 if (_isPrimitive(key)) {
3858 return _primitives[key];
3859 } else {
3860 return _objects[key!] as V?;
3861 }
3862 }
3863
3864 /// Associates the [key] with the given [value].
3865 void operator []=(K key, V value){
3866 if (_isPrimitive(key)) {
3867 _primitives[key] = value;
3868 } else {
3869 _objects[key!] = value;
3870 }
3871 }
3872
3873 /// Removes the value for the given [key] from the map.
3874 V? remove(K key) {
3875 if (_isPrimitive(key)) {
3876 return _primitives.remove(key);
3877 } else {
3878 final V? result = _objects[key!] as V?;
3879 _objects[key] = null;
3880 return result;
3881 }
3882 }
3883
3884 /// Removes all pairs from the map.
3885 void clear() {
3886 _objects = Expando<Object>();
3887 _primitives.clear();
3888 }
3889}
3890