| // 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 _kEdgeHitMargin = 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(_kEdgeHitMargin).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(); |
| } |
| } |
| |
| 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; |
| } |