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