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