// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:developer' as developer; import 'dart:math' as math; import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder; import 'dart:ui' show Offset; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'app.dart'; import 'basic.dart'; import 'binding.dart'; import 'framework.dart'; import 'gesture_detector.dart'; import 'icon_data.dart'; /// Signature for the builder callback used by /// [WidgetInspector.selectButtonBuilder]. typedef Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed); typedef void _RegisterServiceExtensionCallback({ @required String name, @required ServiceExtensionCallback callback }); /// A class describing a step along a path through a tree of [DiagnosticsNode] /// objects. /// /// This class is used to bundle all data required to display the tree with just /// the nodes along a path expanded into a single JSON payload. class _DiagnosticsPathNode { /// Creates a full description of a step in a path through a tree of /// [DiagnosticsNode] objects. /// /// The [node] and [child] arguments must not be null. _DiagnosticsPathNode({ @required this.node, @required this.children, this.childIndex }) : assert(node != null), assert(children != null); /// Node at the point in the path this [_DiagnosticsPathNode] is describing. final DiagnosticsNode node; /// Children of the [node] being described. /// /// This value is cached instead of relying on `node.getChildren()` as that /// method call might create new [DiagnosticsNode] objects for each child /// and we would prefer to use the identical [DiagnosticsNode] for each time /// a node exists in the path. final List<DiagnosticsNode> children; /// Index of the child that the path continues on. /// /// Equal to `null` if the path does not continue. final int childIndex; } List<_DiagnosticsPathNode> _followDiagnosticableChain(List<Diagnosticable> chain, { String name, DiagnosticsTreeStyle style, }) { final List<_DiagnosticsPathNode> path = <_DiagnosticsPathNode>[]; if (chain.isEmpty) return path; DiagnosticsNode diagnostic = chain.first.toDiagnosticsNode(name: name, style: style); for (int i = 1; i < chain.length; i += 1) { final Diagnosticable target = chain[i]; bool foundMatch = false; final List<DiagnosticsNode> children = diagnostic.getChildren(); for (int j = 0; j < children.length; j += 1) { final DiagnosticsNode child = children[j]; if (child.value == target) { foundMatch = true; path.add(new _DiagnosticsPathNode( node: diagnostic, children: children, childIndex: j, )); diagnostic = child; break; } } assert(foundMatch); } path.add(new _DiagnosticsPathNode(node: diagnostic, children: diagnostic.getChildren())); return path; } /// Signature for the selection change callback used by /// [WidgetInspectorService.selectionChangedCallback]. typedef void InspectorSelectionChangedCallback(); /// Structure to help reference count Dart objects referenced by a GUI tool /// using [WidgetInspectorService]. class _InspectorReferenceData { _InspectorReferenceData(this.object); final Object object; int count = 1; } /// Configuration controlling how [DiagnosticsNode] objects are serialized to /// JSON mainly focused on if and how children are included in the JSON. class _SerializeConfig { _SerializeConfig({ @required this.groupName, this.summaryTree = false, this.subtreeDepth = 1, this.pathToInclude, this.includeProperties = false, this.expandPropertyValues = true, }); _SerializeConfig.merge( _SerializeConfig base, { int subtreeDepth, bool omitChildren, Iterable<Diagnosticable> pathToInclude, }) : groupName = base.groupName, summaryTree = base.summaryTree, subtreeDepth = subtreeDepth ?? base.subtreeDepth, pathToInclude = pathToInclude ?? base.pathToInclude, includeProperties = base.includeProperties, expandPropertyValues = base.expandPropertyValues; final String groupName; /// Whether to only include children that would exist in the summary tree. final bool summaryTree; /// How many levels of children to include in the JSON payload. final int subtreeDepth; /// Path of nodes through the children of this node to include even if /// subtreeDepth is exceeded. final Iterable<Diagnosticable> pathToInclude; /// Include information about properties in the JSON instead of requiring /// a separate request to determine properties. final bool includeProperties; /// Expand children of properties that have values that are themselves /// Diagnosticable objects. final bool expandPropertyValues; } class _WidgetInspectorService extends Object with WidgetInspectorService { } /// Service used by GUI tools to interact with the [WidgetInspector]. /// /// Calls to this object are typically made from GUI tools such as the [Flutter /// IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md) /// using the [Dart VM Service protocol](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md). /// This class uses its own object id and manages object lifecycles itself /// instead of depending on the [object ids](https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#getobject) /// specified by the VM Service Protocol because the VM Service Protocol ids /// expire unpredictably. Object references are tracked in groups so that tools /// that clients can use dereference all objects in a group with a single /// operation making it easier to avoid memory leaks. /// /// All methods in this class are appropriate to invoke from debugging tools /// using the Observatory service protocol to evaluate Dart expressions of the /// form `WidgetInspectorService.instance.methodName(arg1, arg2, ...)`. If you /// make changes to any instance method of this class you need to verify that /// the [Flutter IntelliJ Plugin](https://github.com/flutter/flutter-intellij/blob/master/README.md) /// widget inspector support still works with the changes. /// /// All methods returning String values return JSON. class WidgetInspectorService { // This class is usable as a mixin for test purposes and as a singleton // [instance] for production purposes. factory WidgetInspectorService._() => new _WidgetInspectorService(); /// Ring of cached JSON values to prevent json from being garbage /// collected before it can be requested over the Observatory protocol. final List<String> _serializeRing = new List<String>(20); int _serializeRingIndex = 0; /// The current [WidgetInspectorService]. static WidgetInspectorService get instance => _instance; static WidgetInspectorService _instance = new WidgetInspectorService._(); @protected static set instance(WidgetInspectorService instance) { _instance = instance; } static bool _debugServiceExtensionsRegistered = false; /// Ground truth tracking what object(s) are currently selected used by both /// GUI tools such as the Flutter IntelliJ Plugin and the [WidgetInspector] /// displayed on the device. final InspectorSelection selection = new InspectorSelection(); /// Callback typically registered by the [WidgetInspector] to receive /// notifications when [selection] changes. /// /// The Flutter IntelliJ Plugin does not need to listen for this event as it /// instead listens for `dart:developer` `inspect` events which also trigger /// when the inspection target changes on device. InspectorSelectionChangedCallback selectionChangedCallback; /// The Observatory protocol does not keep alive object references so this /// class needs to manually manage groups of objects that should be kept /// alive. final Map<String, Set<_InspectorReferenceData>> _groups = <String, Set<_InspectorReferenceData>>{}; final Map<String, _InspectorReferenceData> _idToReferenceData = <String, _InspectorReferenceData>{}; final Map<Object, String> _objectToId = new Map<Object, String>.identity(); int _nextId = 0; List<String> _pubRootDirectories; _RegisterServiceExtensionCallback _registerServiceExtensionCallback; /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"). /// /// The given callback is called when the extension method is called. The /// callback must return a value that can be converted to JSON using /// `json.encode()` (see [JsonEncoder]). The return value is stored as a /// property named `result` in the JSON. In case of failure, the failure is /// reported to the remote caller and is dumped to the logs. @protected void registerServiceExtension({ @required String name, @required ServiceExtensionCallback callback, }) { _registerServiceExtensionCallback( name: 'inspector.$name', callback: callback, ); } /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"), which takes no arguments. void _registerSignalServiceExtension({ @required String name, @required FutureOr<Object> callback(), }) { registerServiceExtension( name: name, callback: (Map<String, String> parameters) async { return <String, Object>{'result': await callback()}; }, ); } /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"), which takes a single required argument /// "objectGroup" specifying what group is used to manage lifetimes of /// object references in the returned JSON (see [disposeGroup]). void _registerObjectGroupServiceExtension({ @required String name, @required FutureOr<Object> callback(String objectGroup), }) { registerServiceExtension( name: name, callback: (Map<String, String> parameters) async { assert(parameters.containsKey('objectGroup')); return <String, Object>{'result': await callback(parameters['objectGroup'])}; }, ); } /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"), which takes a single argument /// "enabled" which can have the value "true" or the value "false" /// or can be omitted to read the current value. (Any value other /// than "true" is considered equivalent to "false". Other arguments /// are ignored.) /// /// Calls the `getter` callback to obtain the value when /// responding to the service extension method being called. /// /// Calls the `setter` callback with the new value when the /// service extension method is called with a new value. void _registerBoolServiceExtension({ @required String name, @required AsyncValueGetter<bool> getter, @required AsyncValueSetter<bool> setter }) { assert(name != null); assert(getter != null); assert(setter != null); registerServiceExtension( name: name, callback: (Map<String, String> parameters) async { if (parameters.containsKey('enabled')) await setter(parameters['enabled'] == 'true'); return <String, dynamic>{ 'enabled': await getter() ? 'true' : 'false' }; }, ); } /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name") which takes an optional parameter named /// "arg" and a required parameter named "objectGroup" used to control the /// lifetimes of object references in the returned JSON (see [disposeGroup]). void _registerServiceExtensionWithArg({ @required String name, @required FutureOr<Object> callback(String objectId, String objectGroup), }) { registerServiceExtension( name: name, callback: (Map<String, String> parameters) async { assert(parameters.containsKey('objectGroup')); return <String, Object>{ 'result': await callback(parameters['arg'], parameters['objectGroup']), }; }, ); } /// Registers a service extension method with the given name (full /// name "ext.flutter.inspector.name"), that takes arguments /// "arg0", "arg1", "arg2", ..., "argn". void _registerServiceExtensionVarArgs({ @required String name, @required FutureOr<Object> callback(List<String> args), }) { registerServiceExtension( name: name, callback: (Map<String, String> parameters) async { const String argPrefix = 'arg'; final List<String> args = <String>[]; parameters.forEach((String name, String value) { if (name.startsWith(argPrefix)) { final int index = int.parse(name.substring(argPrefix.length)); if (index >= args.length) { args.length = index + 1; } args[index] = value; } }); return <String, Object>{'result': await callback(args)}; }, ); } /// Cause the entire tree to be rebuilt. This is used by development tools /// when the application code has changed and is being hot-reloaded, to cause /// the widget tree to pick up any changed implementations. /// /// This is expensive and should not be called except during development. @protected Future<Null> forceRebuild() { final WidgetsBinding binding = WidgetsBinding.instance; if (binding.renderViewElement != null) { binding.buildOwner.reassemble(binding.renderViewElement); return binding.endOfFrame; } return new Future<Null>.value(); } /// Called to register service extensions. /// /// Service extensions are only exposed when the observatory is /// included in the build, which should only happen in checked mode /// and in profile mode. /// /// See also: /// /// * <https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#rpcs-requests-and-responses> void initServiceExtensions( _RegisterServiceExtensionCallback registerServiceExtensionCallback) { _registerServiceExtensionCallback = registerServiceExtensionCallback; assert(!_debugServiceExtensionsRegistered); assert(() { _debugServiceExtensionsRegistered = true; return true; }()); _registerBoolServiceExtension( name: 'show', getter: () async => WidgetsApp.debugShowWidgetInspectorOverride, setter: (bool value) { if (WidgetsApp.debugShowWidgetInspectorOverride == value) { return new Future<Null>.value(); } WidgetsApp.debugShowWidgetInspectorOverride = value; return forceRebuild(); }, ); _registerSignalServiceExtension( name: 'disposeAllGroups', callback: disposeAllGroups, ); _registerObjectGroupServiceExtension( name: 'disposeGroup', callback: disposeGroup, ); _registerSignalServiceExtension( name: 'isWidgetTreeReady', callback: isWidgetTreeReady, ); _registerServiceExtensionWithArg( name: 'disposeId', callback: disposeId, ); _registerServiceExtensionVarArgs( name: 'setPubRootDirectories', callback: setPubRootDirectories, ); _registerServiceExtensionWithArg( name: 'setSelectionById', callback: setSelectionById, ); _registerServiceExtensionWithArg( name: 'getParentChain', callback: _getParentChain, ); _registerServiceExtensionWithArg( name: 'getProperties', callback: _getProperties, ); _registerServiceExtensionWithArg( name: 'getChildren', callback: _getChildren, ); _registerServiceExtensionWithArg( name: 'getChildrenSummaryTree', callback: _getChildrenSummaryTree, ); _registerServiceExtensionWithArg( name: 'getChildrenDetailsSubtree', callback: _getChildrenDetailsSubtree, ); _registerObjectGroupServiceExtension( name: 'getRootWidget', callback: _getRootWidget, ); _registerObjectGroupServiceExtension( name: 'getRootRenderObject', callback: _getRootRenderObject, ); _registerObjectGroupServiceExtension( name: 'getRootWidgetSummaryTree', callback: _getRootWidgetSummaryTree, ); _registerServiceExtensionWithArg( name: 'getDetailsSubtree', callback: _getDetailsSubtree, ); _registerServiceExtensionWithArg( name: 'getSelectedRenderObject', callback: _getSelectedRenderObject, ); _registerServiceExtensionWithArg( name: 'getSelectedWidget', callback: _getSelectedWidget, ); _registerServiceExtensionWithArg( name: 'getSelectedSummaryWidget', callback: _getSelectedSummaryWidget, ); _registerSignalServiceExtension( name: 'isWidgetCreationTracked', callback: isWidgetCreationTracked, ); } /// Clear all InspectorService object references. /// /// Use this method only for testing to ensure that object references from one /// test case do not impact other test cases. @protected void disposeAllGroups() { _groups.clear(); _idToReferenceData.clear(); _objectToId.clear(); _nextId = 0; } /// Free all references to objects in a group. /// /// Objects and their associated ids in the group may be kept alive by /// references from a different group. @protected void disposeGroup(String name) { final Set<_InspectorReferenceData> references = _groups.remove(name); if (references == null) return; references.forEach(_decrementReferenceCount); } void _decrementReferenceCount(_InspectorReferenceData reference) { reference.count -= 1; assert(reference.count >= 0); if (reference.count == 0) { final String id = _objectToId.remove(reference.object); assert(id != null); _idToReferenceData.remove(id); } } /// Returns a unique id for [object] that will remain live at least until /// [disposeGroup] is called on [groupName] or [dispose] is called on the id /// returned by this method. @protected String toId(Object object, String groupName) { if (object == null) return null; final Set<_InspectorReferenceData> group = _groups.putIfAbsent(groupName, () => new Set<_InspectorReferenceData>.identity()); String id = _objectToId[object]; _InspectorReferenceData referenceData; if (id == null) { id = 'inspector-$_nextId'; _nextId += 1; _objectToId[object] = id; referenceData = new _InspectorReferenceData(object); _idToReferenceData[id] = referenceData; group.add(referenceData); } else { referenceData = _idToReferenceData[id]; if (group.add(referenceData)) referenceData.count += 1; } return id; } /// Returns whether the application has rendered its first frame and it is /// appropriate to display the Widget tree in the inspector. @protected bool isWidgetTreeReady([String groupName]) { return WidgetsBinding.instance != null && WidgetsBinding.instance.debugDidSendFirstFrameEvent; } /// Returns the Dart object associated with a reference id. /// /// The `groupName` parameter is not required by is added to regularize the /// API surface of the methods in this class called from the Flutter IntelliJ /// Plugin. @protected Object toObject(String id, [String groupName]) { if (id == null) return null; final _InspectorReferenceData data = _idToReferenceData[id]; if (data == null) { throw new FlutterError('Id does not exist.'); } return data.object; } /// Returns the object to introspect to determine the source location of an /// object's class. /// /// The Dart object for the id is returned for all cases but [Element] objects /// where the [Widget] configuring the [Element] is returned instead as the /// class of the [Widget] is more relevant than the class of the [Element]. /// /// The `groupName` parameter is not required by is added to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. @protected Object toObjectForSourceLocation(String id, [String groupName]) { final Object object = toObject(id); if (object is Element) { return object.widget; } return object; } /// Remove the object with the specified `id` from the specified object /// group. /// /// If the object exists in other groups it will remain alive and the object /// id will remain valid. @protected void disposeId(String id, String groupName) { if (id == null) return; final _InspectorReferenceData referenceData = _idToReferenceData[id]; if (referenceData == null) throw new FlutterError('Id does not exist'); if (_groups[groupName]?.remove(referenceData) != true) throw new FlutterError('Id is not in group'); _decrementReferenceCount(referenceData); } /// Set the list of directories that should be considered part of the local /// project. /// /// The local project directories are used to distinguish widgets created by /// the local project over widgets created from inside the framework. @protected void setPubRootDirectories(List<Object> pubRootDirectories) { _pubRootDirectories = pubRootDirectories.map<String>( (Object directory) => Uri.parse(directory).path, ).toList(); } /// Set the [WidgetInspector] selection to the object matching the specified /// id if the object is valid object to set as the inspector selection. /// /// Returns `true` if the selection was changed. /// /// The `groupName` parameter is not required by is added to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. @protected bool setSelectionById(String id, [String groupName]) { return setSelection(toObject(id), groupName); } /// Set the [WidgetInspector] selection to the specified `object` if it is /// a valid object to set as the inspector selection. /// /// Returns `true` if the selection was changed. /// /// The `groupName` parameter is not needed but is specified to regularize the /// API surface of methods called from the Flutter IntelliJ Plugin. @protected bool setSelection(Object object, [String groupName]) { if (object is Element || object is RenderObject) { if (object is Element) { if (object == selection.currentElement) { return false; } selection.currentElement = object; } else { if (object == selection.current) { return false; } selection.current = object; } if (selectionChangedCallback != null) { if (WidgetsBinding.instance.schedulerPhase == SchedulerPhase.idle) { selectionChangedCallback(); } else { // It isn't safe to trigger the selection change callback if we are in // the middle of rendering the frame. SchedulerBinding.instance.scheduleTask( selectionChangedCallback, Priority.touch, ); } } return true; } return false; } /// Returns JSON representing the chain of [DiagnosticsNode] instances from /// root of thee tree to the [Element] or [RenderObject] matching `id`. /// /// The JSON contains all information required to display a tree view with /// all nodes other than nodes along the path collapsed. @protected String getParentChain(String id, String groupName) { return _safeJsonEncode(_getParentChain(id, groupName)); } List<Object> _getParentChain(String id, String groupName) { final Object value = toObject(id); List<_DiagnosticsPathNode> path; if (value is RenderObject) path = _getRenderObjectParentChain(value, groupName); else if (value is Element) path = _getElementParentChain(value, groupName); else throw new FlutterError('Cannot get parent chain for node of type ${value.runtimeType}'); return path.map((_DiagnosticsPathNode node) => _pathNodeToJson( node, new _SerializeConfig(groupName: groupName), )).toList(); } Map<String, Object> _pathNodeToJson(_DiagnosticsPathNode pathNode, _SerializeConfig config) { if (pathNode == null) return null; return <String, Object>{ 'node': _nodeToJson(pathNode.node, config), 'children': _nodesToJson(pathNode.children, config), 'childIndex': pathNode.childIndex, }; } List<Element> _getRawElementParentChain(Element element, {int numLocalParents}) { List<Element> elements = element?.debugGetDiagnosticChain(); if (numLocalParents != null) { for (int i = 0; i < elements.length; i += 1) { if (_isValueCreatedByLocalProject(elements[i])) { numLocalParents--; if (numLocalParents <= 0) { elements = elements.take(i + 1).toList(); break; } } } } return elements?.reversed?.toList(); } List<_DiagnosticsPathNode> _getElementParentChain(Element element, String groupName, {int numLocalParents}) { return _followDiagnosticableChain( _getRawElementParentChain(element, numLocalParents: numLocalParents), ) ?? const <_DiagnosticsPathNode>[]; } List<_DiagnosticsPathNode> _getRenderObjectParentChain(RenderObject renderObject, String groupName, {int maxparents}) { final List<RenderObject> chain = <RenderObject>[]; while (renderObject != null) { chain.add(renderObject); renderObject = renderObject.parent; } return _followDiagnosticableChain(chain.reversed.toList()); } Map<String, Object> _nodeToJson( DiagnosticsNode node, _SerializeConfig config, ) { if (node == null) return null; final Map<String, Object> json = node.toJsonMap(); json['objectId'] = toId(node, config.groupName); final Object value = node.value; json['valueId'] = toId(value, config.groupName); if (config.summaryTree) { json['summaryTree'] = true; } final _Location creationLocation = _getCreationLocation(value); bool createdByLocalProject = false; if (creationLocation != null) { json['creationLocation'] = creationLocation.toJsonMap(); if (_isLocalCreationLocation(creationLocation)) { createdByLocalProject = true; json['createdByLocalProject'] = true; } } if (config.subtreeDepth > 0 || (config.pathToInclude != null && config.pathToInclude.isNotEmpty)) { json['children'] = _nodesToJson(_getChildrenHelper(node, config), config); } if (config.includeProperties) { json['properties'] = _nodesToJson( node.getProperties().where( (DiagnosticsNode node) => !node.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info), ), new _SerializeConfig(groupName: config.groupName, subtreeDepth: 1, expandPropertyValues: true), ); } if (node is DiagnosticsProperty) { // Add additional information about properties needed for graphical // display of properties. if (value is Color) { json['valueProperties'] = <String, Object>{ 'red': value.red, 'green': value.green, 'blue': value.blue, 'alpha': value.alpha, }; } else if (value is IconData) { json['valueProperties'] = <String, Object>{ 'codePoint': value.codePoint, }; } if (config.expandPropertyValues && value is Diagnosticable) { json['properties'] = _nodesToJson( value.toDiagnosticsNode().getProperties().where( (DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info), ), new _SerializeConfig(groupName: config.groupName, subtreeDepth: 0, expandPropertyValues: false, ), ); } } return json; } bool _isValueCreatedByLocalProject(Object value) { final _Location creationLocation = _getCreationLocation(value); if (creationLocation == null) { return false; } return _isLocalCreationLocation(creationLocation); } bool _isLocalCreationLocation(_Location location) { if (_pubRootDirectories == null || location == null || location.file == null) { return false; } final String file = Uri.parse(location.file).path; for (String directory in _pubRootDirectories) { if (file.startsWith(directory)) { return true; } } return false; } /// Wrapper around `json.encode` that uses a ring of cached values to prevent /// the Dart garbage collector from collecting objects between when /// the value is returned over the Observatory protocol and when the /// separate observatory protocol command has to be used to retrieve its full /// contents. /// TODO(jacobr): Replace this with a better solution once /// https://github.com/dart-lang/sdk/issues/32919 is fixed. String _safeJsonEncode(Object object) { final String jsonString = json.encode(object); _serializeRing[_serializeRingIndex] = jsonString; _serializeRingIndex = (_serializeRingIndex + 1) % _serializeRing.length; return jsonString; } List<Map<String, Object>> _nodesToJson( Iterable<DiagnosticsNode> nodes, _SerializeConfig config, ) { if (nodes == null) return <Map<String, Object>>[]; return nodes.map<Map<String, Object>>( (DiagnosticsNode node) { if (config.pathToInclude != null && config.pathToInclude.isNotEmpty) { if (config.pathToInclude.first == node.value) { return _nodeToJson( node, new _SerializeConfig.merge(config, pathToInclude: config.pathToInclude.skip(1)), ); } else { return _nodeToJson(node, new _SerializeConfig.merge(config, omitChildren: true)); } } // The tricky special case here is that when in the detailsTree, // we keep subtreeDepth from going down to zero until we reach nodes // that also exist in the summary tree. This ensures that every time // you expand a node in the details tree, you expand the entire subtree // up until you reach the next nodes shared with the summary tree. return _nodeToJson( node, config.summaryTree || config.subtreeDepth > 1 || _shouldShowInSummaryTree(node) ? new _SerializeConfig.merge(config, subtreeDepth: config.subtreeDepth - 1) : config, ); }).toList(); } /// Returns a JSON representation of the properties of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references. @protected String getProperties(String diagnosticsNodeId, String groupName) { return _safeJsonEncode(_getProperties(diagnosticsNodeId, groupName)); } List<Object> _getProperties(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : node.getProperties(), new _SerializeConfig(groupName: groupName)); } /// Returns a JSON representation of the children of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references. String getChildren(String diagnosticsNodeId, String groupName) { return _safeJsonEncode(_getChildren(diagnosticsNodeId, groupName)); } List<Object> _getChildren(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); final _SerializeConfig config = new _SerializeConfig(groupName: groupName); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config); } /// Returns a JSON representation of the children of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references only including children that /// were created directly by user code. /// /// Requires [Widget] creation locations which are only available for debug /// mode builds when the `--track-widget-creation` flag is passed to /// `flutter_tool`. /// /// See also: /// /// * [isWidgetCreationTracked] which indicates whether this method can be /// used. String getChildrenSummaryTree(String diagnosticsNodeId, String groupName) { return _safeJsonEncode(_getChildrenSummaryTree(diagnosticsNodeId, groupName)); } List<Object> _getChildrenSummaryTree(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); final _SerializeConfig config = new _SerializeConfig(groupName: groupName, summaryTree: true); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config); } /// Returns a JSON representation of the children of the [DiagnosticsNode] /// object that `diagnosticsNodeId` references providing information needed /// for the details subtree view. /// /// The details subtree shows properties inline and includes all children /// rather than a filtered set of important children. String getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) { return _safeJsonEncode(_getChildrenDetailsSubtree(diagnosticsNodeId, groupName)); } List<Object> _getChildrenDetailsSubtree(String diagnosticsNodeId, String groupName) { final DiagnosticsNode node = toObject(diagnosticsNodeId); // With this value of minDepth we only expand one extra level of important nodes. final _SerializeConfig config = new _SerializeConfig(groupName: groupName, subtreeDepth: 1, includeProperties: true); return _nodesToJson(node == null ? const <DiagnosticsNode>[] : _getChildrenHelper(node, config), config); } List<DiagnosticsNode> _getChildrenHelper(DiagnosticsNode node, _SerializeConfig config) { return _getChildrenFiltered(node, config).toList(); } bool _shouldShowInSummaryTree(DiagnosticsNode node) { final Object value = node.value; if (value is! Diagnosticable) { return true; } if (value is! Element || !isWidgetCreationTracked()) { // Creation locations are not availabe so include all nodes in the // summary tree. return true; } return _isValueCreatedByLocalProject(value); } List<DiagnosticsNode> _getChildrenFiltered( DiagnosticsNode node, _SerializeConfig config, ) { final List<DiagnosticsNode> children = <DiagnosticsNode>[]; for (DiagnosticsNode child in node.getChildren()) { if (!config.summaryTree || _shouldShowInSummaryTree(child)) { children.add(child); } else { children.addAll(_getChildrenFiltered(child, config)); } } return children; } /// Returns a JSON representation of the [DiagnosticsNode] for the root /// [Element]. String getRootWidget(String groupName) { return _safeJsonEncode(_getRootWidget(groupName)); } Map<String, Object> _getRootWidget(String groupName) { return _nodeToJson(WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName)); } /// Returns a JSON representation of the [DiagnosticsNode] for the root /// [Element] showing only nodes that should be included in a summary tree. String getRootWidgetSummaryTree(String groupName) { return _safeJsonEncode(_getRootWidgetSummaryTree(groupName)); } Map<String, Object> _getRootWidgetSummaryTree(String groupName) { return _nodeToJson( WidgetsBinding.instance?.renderViewElement?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName, subtreeDepth: 1000000, summaryTree: true), ); } /// Returns a JSON representation of the [DiagnosticsNode] for the root /// [RenderObject]. @protected String getRootRenderObject(String groupName) { return _safeJsonEncode(_getRootRenderObject(groupName)); } Map<String, Object> _getRootRenderObject(String groupName) { return _nodeToJson(RendererBinding.instance?.renderView?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName)); } /// Returns a JSON representation of the subtree rooted at the /// [DiagnosticsNode] object that `diagnosticsNodeId` references providing /// information needed for the details subtree view. /// /// See also: /// * [getChildrenDetailsSubtree], a method to get children of a node /// in the details subtree. String getDetailsSubtree(String id, String groupName) { return _safeJsonEncode(_getDetailsSubtree( id, groupName)); } Map<String, Object> _getDetailsSubtree(String id, String groupName) { final DiagnosticsNode root = toObject(id); if (root == null) { return null; } return _nodeToJson( root, new _SerializeConfig( groupName: groupName, summaryTree: false, subtreeDepth: 2, // TODO(jacobr): make subtreeDepth configurable. includeProperties: true, ), ); } /// Returns a [DiagnosticsNode] representing the currently selected /// [RenderObject]. /// /// If the currently selected [RenderObject] is identical to the /// [RenderObject] referenced by `previousSelectionId` then the previous /// [DiagnosticNode] is reused. @protected String getSelectedRenderObject(String previousSelectionId, String groupName) { return _safeJsonEncode(_getSelectedRenderObject(previousSelectionId, groupName)); } Map<String, Object> _getSelectedRenderObject(String previousSelectionId, String groupName) { final DiagnosticsNode previousSelection = toObject(previousSelectionId); final RenderObject current = selection?.current; return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName)); } /// Returns a [DiagnosticsNode] representing the currently selected [Element]. /// /// If the currently selected [Element] is identical to the [Element] /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is /// reused. @protected String getSelectedWidget(String previousSelectionId, String groupName) { return _safeJsonEncode(_getSelectedWidget(previousSelectionId, groupName)); } Map<String, Object> _getSelectedWidget(String previousSelectionId, String groupName) { final DiagnosticsNode previousSelection = toObject(previousSelectionId); final Element current = selection?.currentElement; return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName)); } /// Returns a [DiagnosticsNode] representing the currently selected [Element] /// if the selected [Element] should be shown in the summary tree otherwise /// returns the first ancestor of the selected [Element] shown in the summary /// tree. /// /// If the currently selected [Element] is identical to the [Element] /// referenced by `previousSelectionId` then the previous [DiagnosticNode] is /// reused. String getSelectedSummaryWidget(String previousSelectionId, String groupName) { return _safeJsonEncode(_getSelectedSummaryWidget(previousSelectionId, groupName)); } Map<String, Object> _getSelectedSummaryWidget(String previousSelectionId, String groupName) { if (!isWidgetCreationTracked()) { return _getSelectedWidget(previousSelectionId, groupName); } final DiagnosticsNode previousSelection = toObject(previousSelectionId); Element current = selection?.currentElement; if (current != null && !_isValueCreatedByLocalProject(current)) { Element firstLocal; for (Element candidate in current.debugGetDiagnosticChain()) { if (_isValueCreatedByLocalProject(candidate)) { firstLocal = candidate; break; } } current = firstLocal; } return _nodeToJson(current == previousSelection?.value ? previousSelection : current?.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName)); } /// Returns whether [Widget] creation locations are available. /// /// [Widget] creation locations are only available for debug mode builds when /// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 /// is required as injecting creation locations requires a /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). @protected bool isWidgetCreationTracked() { _widgetCreationTracked ??= new _WidgetForTypeTests() is _HasCreationLocation; return _widgetCreationTracked; } bool _widgetCreationTracked; } class _WidgetForTypeTests extends Widget { @override Element createElement() => null; } /// A widget that enables inspecting the child widget's structure. /// /// Select a location on your device or emulator and view what widgets and /// render object that best matches the location. An outline of the selected /// widget and terse summary information is shown on device with detailed /// information is shown in the observatory or in IntelliJ when using the /// Flutter Plugin. /// /// The inspector has a select mode and a view mode. /// /// In the select mode, tapping the device selects the widget that best matches /// the location of the touch and switches to view mode. Dragging a finger on /// the device selects the widget under the drag location but does not switch /// modes. Touching the very edge of the bounding box of a widget triggers /// selecting the widget even if another widget that also overlaps that /// location would otherwise have priority. /// /// In the view mode, the previously selected widget is outlined, however, /// touching the device has the same effect it would have if the inspector /// wasn't present. This allows interacting with the application and viewing how /// the selected widget changes position. Clicking on the select icon in the /// bottom left corner of the application switches back to select mode. class WidgetInspector extends StatefulWidget { /// Creates a widget that enables inspection for the child. /// /// The [child] argument must not be null. const WidgetInspector({ Key key, @required this.child, @required this.selectButtonBuilder, }) : assert(child != null), super(key: key); /// The widget that is being inspected. final Widget child; /// A builder that is called to create the select button. /// /// The `onPressed` callback passed as an argument to the builder should be /// hooked up to the returned widget. final InspectorSelectButtonBuilder selectButtonBuilder; @override _WidgetInspectorState createState() => new _WidgetInspectorState(); } class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingObserver { _WidgetInspectorState() : selection = WidgetInspectorService.instance.selection; Offset _lastPointerLocation; final InspectorSelection selection; /// Whether the inspector is in select mode. /// /// In select mode, pointer interactions trigger widget selection instead of /// normal interactions. Otherwise the previously selected widget is /// highlighted but the application can be interacted with normally. bool isSelectMode = true; final GlobalKey _ignorePointerKey = new GlobalKey(); /// Distance from the edge of of the bounding box for an element to consider /// as selecting the edge of the bounding box. static const double _edgeHitMargin = 2.0; InspectorSelectionChangedCallback _selectionChangedCallback; @override void initState() { super.initState(); _selectionChangedCallback = () { setState(() { // The [selection] property which the build method depends on has // changed. }); }; assert(WidgetInspectorService.instance.selectionChangedCallback == null); WidgetInspectorService.instance.selectionChangedCallback = _selectionChangedCallback; } @override void dispose() { if (WidgetInspectorService.instance.selectionChangedCallback == _selectionChangedCallback) { WidgetInspectorService.instance.selectionChangedCallback = null; } super.dispose(); } bool _hitTestHelper( List<RenderObject> hits, List<RenderObject> edgeHits, Offset position, RenderObject object, Matrix4 transform, ) { bool hit = false; final Matrix4 inverse = Matrix4.tryInvert(transform); if (inverse == null) { // We cannot invert the transform. That means the object doesn't appear on // screen and cannot be hit. return false; } final Offset localPosition = MatrixUtils.transformPoint(inverse, position); final List<DiagnosticsNode> children = object.debugDescribeChildren(); for (int i = children.length - 1; i >= 0; i -= 1) { final DiagnosticsNode diagnostics = children[i]; assert(diagnostics != null); if (diagnostics.style == DiagnosticsTreeStyle.offstage || diagnostics.value is! RenderObject) continue; final RenderObject child = diagnostics.value; final Rect paintClip = object.describeApproximatePaintClip(child); if (paintClip != null && !paintClip.contains(localPosition)) continue; final Matrix4 childTransform = transform.clone(); object.applyPaintTransform(child, childTransform); if (_hitTestHelper(hits, edgeHits, position, child, childTransform)) hit = true; } final Rect bounds = object.semanticBounds; if (bounds.contains(localPosition)) { hit = true; // Hits that occur on the edge of the bounding box of an object are // given priority to provide a way to select objects that would // otherwise be hard to select. if (!bounds.deflate(_edgeHitMargin).contains(localPosition)) edgeHits.add(object); } if (hit) hits.add(object); return hit; } /// Returns the list of render objects located at the given position ordered /// by priority. /// /// All render objects that are not offstage that match the location are /// included in the list of matches. Priority is given to matches that occur /// on the edge of a render object's bounding box and to matches found by /// [RenderBox.hitTest]. List<RenderObject> hitTest(Offset position, RenderObject root) { final List<RenderObject> regularHits = <RenderObject>[]; final List<RenderObject> edgeHits = <RenderObject>[]; _hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null)); // Order matches by the size of the hit area. double _area(RenderObject object) { final Size size = object.semanticBounds?.size; return size == null ? double.maxFinite : size.width * size.height; } regularHits.sort((RenderObject a, RenderObject b) => _area(a).compareTo(_area(b))); final Set<RenderObject> hits = new LinkedHashSet<RenderObject>(); hits..addAll(edgeHits)..addAll(regularHits); return hits.toList(); } void _inspectAt(Offset position) { if (!isSelectMode) return; final RenderIgnorePointer ignorePointer = _ignorePointerKey.currentContext.findRenderObject(); final RenderObject userRender = ignorePointer.child; final List<RenderObject> selected = hitTest(position, userRender); setState(() { selection.candidates = selected; }); } void _handlePanDown(DragDownDetails event) { _lastPointerLocation = event.globalPosition; _inspectAt(event.globalPosition); } void _handlePanUpdate(DragUpdateDetails event) { _lastPointerLocation = event.globalPosition; _inspectAt(event.globalPosition); } void _handlePanEnd(DragEndDetails details) { // If the pan ends on the edge of the window assume that it indicates the // pointer is being dragged off the edge of the display not a regular touch // on the edge of the display. If the pointer is being dragged off the edge // of the display we do not want to select anything. A user can still select // a widget that is only at the exact screen margin by tapping. final Rect bounds = (Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio)).deflate(_kOffScreenMargin); if (!bounds.contains(_lastPointerLocation)) { setState(() { selection.clear(); }); } } void _handleTap() { if (!isSelectMode) return; if (_lastPointerLocation != null) { _inspectAt(_lastPointerLocation); if (selection != null) { // Notify debuggers to open an inspector on the object. developer.inspect(selection.current); } } setState(() { // Only exit select mode if there is a button to return to select mode. if (widget.selectButtonBuilder != null) isSelectMode = false; }); } void _handleEnableSelect() { setState(() { isSelectMode = true; }); } @override Widget build(BuildContext context) { final List<Widget> children = <Widget>[]; children.add(new GestureDetector( onTap: _handleTap, onPanDown: _handlePanDown, onPanEnd: _handlePanEnd, onPanUpdate: _handlePanUpdate, behavior: HitTestBehavior.opaque, excludeFromSemantics: true, child: new IgnorePointer( ignoring: isSelectMode, key: _ignorePointerKey, ignoringSemantics: false, child: widget.child, ), )); if (!isSelectMode && widget.selectButtonBuilder != null) { children.add(new Positioned( left: _kInspectButtonMargin, bottom: _kInspectButtonMargin, child: widget.selectButtonBuilder(context, _handleEnableSelect) )); } children.add(new _InspectorOverlay(selection: selection)); return new Stack(children: children); } } /// Mutable selection state of the inspector. class InspectorSelection { /// Render objects that are candidates to be selected. /// /// Tools may wish to iterate through the list of candidates. List<RenderObject> get candidates => _candidates; List<RenderObject> _candidates = <RenderObject>[]; set candidates(List<RenderObject> value) { _candidates = value; _index = 0; _computeCurrent(); } /// Index within the list of candidates that is currently selected. int get index => _index; int _index = 0; set index(int value) { _index = value; _computeCurrent(); } /// Set the selection to empty. void clear() { _candidates = <RenderObject>[]; _index = 0; _computeCurrent(); } /// Selected render object typically from the [candidates] list. /// /// Setting [candidates] or calling [clear] resets the selection. /// /// Returns null if the selection is invalid. RenderObject get current => _current; RenderObject _current; set current(RenderObject value) { if (_current != value) { _current = value; _currentElement = value.debugCreator.element; } } /// Selected [Element] consistent with the [current] selected [RenderObject]. /// /// Setting [candidates] or calling [clear] resets the selection. /// /// Returns null if the selection is invalid. Element get currentElement => _currentElement; Element _currentElement; set currentElement(Element element) { if (currentElement != element) { _currentElement = element; _current = element.findRenderObject(); } } void _computeCurrent() { if (_index < candidates.length) { _current = candidates[index]; _currentElement = _current.debugCreator.element; } else { _current = null; _currentElement = null; } } /// Whether the selected render object is attached to the tree or has gone /// out of scope. bool get active => _current != null && _current.attached; } class _InspectorOverlay extends LeafRenderObjectWidget { const _InspectorOverlay({ Key key, @required this.selection, }) : super(key: key); final InspectorSelection selection; @override _RenderInspectorOverlay createRenderObject(BuildContext context) { return new _RenderInspectorOverlay(selection: selection); } @override void updateRenderObject(BuildContext context, _RenderInspectorOverlay renderObject) { renderObject.selection = selection; } } class _RenderInspectorOverlay extends RenderBox { /// The arguments must not be null. _RenderInspectorOverlay({ @required InspectorSelection selection }) : _selection = selection, assert(selection != null); InspectorSelection get selection => _selection; InspectorSelection _selection; set selection(InspectorSelection value) { if (value != _selection) { _selection = value; } markNeedsPaint(); } @override bool get sizedByParent => true; @override bool get alwaysNeedsCompositing => true; @override void performResize() { size = constraints.constrain(const Size(double.infinity, double.infinity)); } @override void paint(PaintingContext context, Offset offset) { assert(needsCompositing); context.addLayer(new _InspectorOverlayLayer( overlayRect: new Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height), selection: selection, )); } } class _TransformedRect { _TransformedRect(RenderObject object) : rect = object.semanticBounds, transform = object.getTransformTo(null); final Rect rect; final Matrix4 transform; @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final _TransformedRect typedOther = other; return rect == typedOther.rect && transform == typedOther.transform; } @override int get hashCode => hashValues(rect, transform); } /// State describing how the inspector overlay should be rendered. /// /// The equality operator can be used to determine whether the overlay needs to /// be rendered again. class _InspectorOverlayRenderState { _InspectorOverlayRenderState({ @required this.overlayRect, @required this.selected, @required this.candidates, @required this.tooltip, @required this.textDirection, }); final Rect overlayRect; final _TransformedRect selected; final List<_TransformedRect> candidates; final String tooltip; final TextDirection textDirection; @override bool operator ==(dynamic other) { if (other.runtimeType != runtimeType) return false; final _InspectorOverlayRenderState typedOther = other; return overlayRect == typedOther.overlayRect && selected == typedOther.selected && listEquals<_TransformedRect>(candidates, typedOther.candidates) && tooltip == typedOther.tooltip; } @override int get hashCode => hashValues(overlayRect, selected, hashList(candidates), tooltip); } const int _kMaxTooltipLines = 5; const Color _kTooltipBackgroundColor = const Color.fromARGB(230, 60, 60, 60); const Color _kHighlightedRenderObjectFillColor = const Color.fromARGB(128, 128, 128, 255); const Color _kHighlightedRenderObjectBorderColor = const Color.fromARGB(128, 64, 64, 128); /// A layer that outlines the selected [RenderObject] and candidate render /// objects that also match the last pointer location. /// /// This approach is horrific for performance and is only used here because this /// is limited to debug mode. Do not duplicate the logic in production code. class _InspectorOverlayLayer extends Layer { /// Creates a layer that displays the inspector overlay. _InspectorOverlayLayer({ @required this.overlayRect, @required this.selection, }) : assert(overlayRect != null), assert(selection != null) { bool inDebugMode = false; assert(() { inDebugMode = true; return true; }()); if (inDebugMode == false) { throw new FlutterError( 'The inspector should never be used in production mode due to the ' 'negative performance impact.' ); } } InspectorSelection selection; /// The rectangle in this layer's coordinate system that the overlay should /// occupy. /// /// The scene must be explicitly recomposited after this property is changed /// (as described at [Layer]). final Rect overlayRect; _InspectorOverlayRenderState _lastState; /// Picture generated from _lastState. ui.Picture _picture; TextPainter _textPainter; double _textPainterMaxWidth; @override void addToScene(ui.SceneBuilder builder, Offset layerOffset) { if (!selection.active) return; final RenderObject selected = selection.current; final List<_TransformedRect> candidates = <_TransformedRect>[]; for (RenderObject candidate in selection.candidates) { if (candidate == selected || !candidate.attached) continue; candidates.add(new _TransformedRect(candidate)); } final _InspectorOverlayRenderState state = new _InspectorOverlayRenderState( overlayRect: overlayRect, selected: new _TransformedRect(selected), tooltip: selection.currentElement.toStringShort(), textDirection: TextDirection.ltr, candidates: candidates, ); if (state != _lastState) { _lastState = state; _picture = _buildPicture(state); } builder.addPicture(layerOffset, _picture); } ui.Picture _buildPicture(_InspectorOverlayRenderState state) { final ui.PictureRecorder recorder = new ui.PictureRecorder(); final Canvas canvas = new Canvas(recorder, state.overlayRect); final Size size = state.overlayRect.size; final Paint fillPaint = new Paint() ..style = PaintingStyle.fill ..color = _kHighlightedRenderObjectFillColor; final Paint borderPaint = new Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.0 ..color = _kHighlightedRenderObjectBorderColor; // Highlight the selected renderObject. final Rect selectedPaintRect = state.selected.rect.deflate(0.5); canvas ..save() ..transform(state.selected.transform.storage) ..drawRect(selectedPaintRect, fillPaint) ..drawRect(selectedPaintRect, borderPaint) ..restore(); // Show all other candidate possibly selected elements. This helps selecting // render objects by selecting the edge of the bounding box shows all // elements the user could toggle the selection between. for (_TransformedRect transformedRect in state.candidates) { canvas ..save() ..transform(transformedRect.transform.storage) ..drawRect(transformedRect.rect.deflate(0.5), borderPaint) ..restore(); } final Rect targetRect = MatrixUtils.transformRect( state.selected.transform, state.selected.rect); final Offset target = new Offset(targetRect.left, targetRect.center.dy); const double offsetFromWidget = 9.0; final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget; _paintDescription(canvas, state.tooltip, state.textDirection, target, verticalOffset, size, targetRect); // TODO(jacobr): provide an option to perform a debug paint of just the // selected widget. return recorder.endRecording(); } void _paintDescription( Canvas canvas, String message, TextDirection textDirection, Offset target, double verticalOffset, Size size, Rect targetRect, ) { canvas.save(); final double maxWidth = size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding); if (_textPainter == null || _textPainter.text.text != message || _textPainterMaxWidth != maxWidth) { _textPainterMaxWidth = maxWidth; _textPainter = new TextPainter() ..maxLines = _kMaxTooltipLines ..ellipsis = '...' ..text = new TextSpan(style: _messageStyle, text: message) ..textDirection = textDirection ..layout(maxWidth: maxWidth); } final Size tooltipSize = _textPainter.size + const Offset(_kTooltipPadding * 2, _kTooltipPadding * 2); final Offset tipOffset = positionDependentBox( size: size, childSize: tooltipSize, target: target, verticalOffset: verticalOffset, preferBelow: false, ); final Paint tooltipBackground = new Paint() ..style = PaintingStyle.fill ..color = _kTooltipBackgroundColor; canvas.drawRect( new Rect.fromPoints( tipOffset, tipOffset.translate(tooltipSize.width, tooltipSize.height), ), tooltipBackground, ); double wedgeY = tipOffset.dy; final bool tooltipBelow = tipOffset.dy > target.dy; if (!tooltipBelow) wedgeY += tooltipSize.height; const double wedgeSize = _kTooltipPadding * 2; double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2; wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2); final List<Offset> wedge = <Offset>[ new Offset(wedgeX - wedgeSize, wedgeY), new Offset(wedgeX + wedgeSize, wedgeY), new Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)), ]; canvas.drawPath(new Path()..addPolygon(wedge, true,), tooltipBackground); _textPainter.paint(canvas, tipOffset + const Offset(_kTooltipPadding, _kTooltipPadding)); canvas.restore(); } @override S find<S>(Offset regionOffset) => null; } const double _kScreenEdgeMargin = 10.0; const double _kTooltipPadding = 5.0; const double _kInspectButtonMargin = 10.0; /// Interpret pointer up events within with this margin as indicating the /// pointer is moving off the device. const double _kOffScreenMargin = 1.0; const TextStyle _messageStyle = const TextStyle( color: const Color(0xFFFFFFFF), fontSize: 10.0, height: 1.2, ); /// Interface for classes that track the source code location the their /// constructor was called from. /// /// A [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). /// adds this interface to the [Widget] class when the /// `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is /// required as injecting creation locations requires a /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). // ignore: unused_element abstract class _HasCreationLocation { _Location get _location; } /// A tuple with file, line, and column number, for displaying human-readable /// file locations. class _Location { const _Location({ this.file, this.line, this.column, this.name, this.parameterLocations }); /// File path of the location. final String file; /// 1-based line number. final int line; /// 1-based column number. final int column; /// Optional name of the parameter or function at this location. final String name; /// Optional locations of the parameters of the member at this location. final List<_Location> parameterLocations; Map<String, Object> toJsonMap() { final Map<String, Object> json = <String, Object>{ 'file': file, 'line': line, 'column': column, }; if (name != null) { json['name'] = name; } if (parameterLocations != null) { json['parameterLocations'] = parameterLocations.map<Map<String, Object>>( (_Location location) => location.toJsonMap()).toList(); } return json; } @override String toString() { final List<String> parts = <String>[]; if (name != null) { parts.add(name); } if (file != null) { parts.add(file); } parts..add('$line')..add('$column'); return parts.join(':'); } } /// Returns the creation location of an object if one is available. /// /// Creation locations are only available for debug mode builds when /// the `--track-widget-creation` flag is passed to `flutter_tool`. Dart 2.0 is /// required as injecting creation locations requires a /// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation). /// /// Currently creation locations are only available for [Widget] and [Element] _Location _getCreationLocation(Object object) { final Object candidate = object is Element ? object.widget : object; return candidate is _HasCreationLocation ? candidate._location : null; }