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