using System; using System.Collections.Generic; using System.Linq; using RSG; using Unity.UIWidgets.foundation; using Unity.UIWidgets.gestures; using Unity.UIWidgets.rendering; using Unity.UIWidgets.scheduler; using Unity.UIWidgets.ui; namespace Unity.UIWidgets.widgets { public delegate Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed); class _SerializeConfig { public _SerializeConfig(string groupName, bool summaryTree = false, int subtreeDepth = 1, List pathToInclude = null, bool includeProperties = false, bool expandPropertyValues = true) { this.groupName = groupName; this.summaryTree = summaryTree; this.subtreeDepth = subtreeDepth; this.pathToInclude = pathToInclude; this.includeProperties = includeProperties; this.expandPropertyValues = expandPropertyValues; } public static _SerializeConfig merge(_SerializeConfig from, int? subtreeDepth = null, List pathToInclude = null) { return new _SerializeConfig(from.groupName, from.summaryTree, subtreeDepth ?? from.subtreeDepth, pathToInclude ?? from.pathToInclude, from.includeProperties, from.expandPropertyValues); } public readonly string groupName; public readonly bool summaryTree; public readonly int subtreeDepth; public readonly List pathToInclude; public readonly bool includeProperties; public readonly bool expandPropertyValues; } public delegate void InspectorSelectionChangedCallback(); public delegate void InspectorShowCallback(); public delegate void DeveloperInspect(); class _InspectorReferenceData { public readonly object obj; public int count = 1; public _InspectorReferenceData(object obj) { this.obj = obj; } } public class WidgetInspectorService { public readonly WidgetsBinding widgetsBinding; bool _debugShowInspector; readonly Dictionary> _groups = new Dictionary>(); readonly Dictionary _idToReferenceData = new Dictionary(); readonly Dictionary _objectToId = new Dictionary(); int _nextId = 0; public readonly InspectorSelection selection = new InspectorSelection(); public InspectorSelectionChangedCallback selectionChangedCallback; public DeveloperInspect developerInspect; public InspectorShowCallback inspectorShowCallback; public static WidgetInspectorService instance { get { return WidgetsBinding.instance.widgetInspectorService; } } public WidgetInspectorService(WidgetsBinding widgetsBinding) { this.widgetsBinding = widgetsBinding; } // private static WidgetInspectorService _instance; // // public WidgetInspectorService instance // { // get { return _instance; } // } public Dictionary getRootWidget(string groupName) { return this._nodeToJson(this.widgetsBinding.renderViewElement.toDiagnosticsNode(), new _SerializeConfig(groupName)); } public Dictionary getRootWidgetSummaryTree(string groupName) { return this._nodeToJson(this.widgetsBinding.renderViewElement.toDiagnosticsNode(), new _SerializeConfig(groupName, subtreeDepth: 1000000, summaryTree: true)); } public Dictionary getRootRenderObject(string groupName) { return this._nodeToJson(this.widgetsBinding.renderView.toDiagnosticsNode(), new _SerializeConfig(groupName: groupName, subtreeDepth: 1000000)); } public Dictionary getDetailsSubtree(string id, string groupName) { var root = this.toObject(id) as DiagnosticsNode; if (root == null) { return null; } return this._nodeToJson(root, new _SerializeConfig(groupName, summaryTree: false, subtreeDepth: 2, includeProperties: true)); } public bool setSelectionById(string id, string groupName = "") { return this.setSelection(this.toObject(id), groupName); } public Dictionary getSelectedRenderObject(string previousSelectionId, string groupName) { DiagnosticsNode previousSelection = this.toObject(previousSelectionId) as DiagnosticsNode; RenderObject current = this.selection == null ? null : this.selection.current; return this._nodeToJson( current == (previousSelection == null ? null : previousSelection.valueObject) ? previousSelection : (current == null ? null : current.toDiagnosticsNode()), new _SerializeConfig(groupName: groupName)); } public Dictionary getSelectedWidget(string previousSelectionId, string groupName) { DiagnosticsNode previousSelection = (DiagnosticsNode) this.toObject(previousSelectionId); Element current = null; if (this.selection != null) { current = this.selection.currentElement; } return this._nodeToJson( current == (previousSelection == null ? null : previousSelection.valueObject) ? previousSelection : (current == null ? null : current.toDiagnosticsNode()), new _SerializeConfig(groupName: groupName)); } public Dictionary getSelectedSummaryWidget(string previousSelectionId, string groupName) { return this.getSelectedWidget(previousSelectionId, groupName); } public bool debugShowInspector { get { return this._debugShowInspector; } set { var old = this._debugShowInspector; this._debugShowInspector = value; if (this._debugShowInspector != old && this.inspectorShowCallback != null) { this.inspectorShowCallback(); } } } public void disposeGroup(string groupName) { HashSet<_InspectorReferenceData> references; this._groups.TryGetValue(groupName, out references); this._groups.Remove(groupName); if (references != null) { foreach (var r in references) { this._decrementReferenceCount(r); } } D.assert(() => { // var groupNames = _groups.Select((entry) => string.Format("groupName={0} count={1}", entry.Key, entry.Value.Count)); // Debug.LogFormat("groups {0} idsCount={1} objectsCount={2}", string.Join(",", groupNames.ToArray()), // _idToReferenceData.Count, _objectToId.Count); return true; }); } protected bool setSelection(object obj, string groupName = "") { if (obj is Element || obj is RenderObject) { if (obj is Element) { if (ReferenceEquals(obj, this.selection.currentElement)) { return false; } this.selection.currentElement = (Element) obj; } else { if (obj == this.selection.current) { return false; } this.selection.current = (RenderObject) obj; } if (this.selectionChangedCallback != null) { if (WidgetsBinding.instance.schedulerPhase == SchedulerPhase.idle) { this.selectionChangedCallback(); } else { // todo schedule task ? WidgetsBinding.instance.scheduleFrameCallback( duration => { this.selectionChangedCallback(); } ); } } return true; } return false; } Dictionary _nodeToJson(DiagnosticsNode node, _SerializeConfig config) { if (node == null) { return null; } var ret = node.toJsonMap(); var value = node.valueObject; ret["objectId"] = this.toId(node, config.groupName); ret["valueId"] = this.toId(value, config.groupName); if (config.summaryTree) { ret["summaryTree"] = config.summaryTree; } var createdByLocalProject = true; // todo; if (config.subtreeDepth > 0 || (config.pathToInclude != null && config.pathToInclude.Count > 0)) { ret["children"] = this._nodesToJson(this._getChildrenFiltered(node, config), config); } if (config.includeProperties) { ret["properties"] = this._nodesToJson( node.getProperties().Where((n) => !n.isFiltered(createdByLocalProject ? DiagnosticLevel.fine : DiagnosticLevel.info)).ToList(), new _SerializeConfig(groupName: config.groupName, subtreeDepth: 1, expandPropertyValues: true) ); } var typeDef = typeof(DiagnosticsProperty<>); var nodeType = node.GetType(); if (nodeType.IsGenericType && nodeType.GetGenericTypeDefinition() == typeDef) { if (value is Color) { ret["valueProperties"] = new Dictionary { {"red", ((Color) value).red}, {"green", ((Color) value).green}, {"blue", ((Color) value).blue}, {"alpha", ((Color) value).alpha}, }; } else if (value is IconData) { ret["valueProperties"] = new Dictionary { {"codePoint", ((IconData) value).codePoint} }; } if (config.expandPropertyValues && value is Diagnosticable) { ret["properties"] = this._nodesToJson( ((Diagnosticable) value).toDiagnosticsNode().getProperties() .Where(n => !n.isFiltered(DiagnosticLevel.info)).ToList(), new _SerializeConfig(groupName: config.groupName, subtreeDepth: 0, expandPropertyValues: false) ); } } return ret; } List> _nodesToJson(List nodes, _SerializeConfig config) { if (nodes == null) { return new List>(); } return nodes.Select(node => { if (config.pathToInclude != null && config.pathToInclude.Count > 0) { if (config.pathToInclude[0] == node.valueObject) { return this._nodeToJson(node, _SerializeConfig.merge(config, pathToInclude: config.pathToInclude.GetRange(1, config.pathToInclude.Count - 1))); } else { return this._nodeToJson(node, _SerializeConfig.merge(config)); } } return this._nodeToJson(node, config.summaryTree || config.subtreeDepth > 1 || this._shouldShowInSummaryTree(node) ? _SerializeConfig.merge(config, subtreeDepth: config.subtreeDepth - 1) : config ); }).ToList(); } public List> getProperties(string diagnosticsNodeId, string groupName) { var node = this.toObject(diagnosticsNodeId) as DiagnosticsNode; return this._nodesToJson(node == null ? new List() : node.getProperties(), new _SerializeConfig(groupName: groupName)); } List _getChildrenFiltered(DiagnosticsNode node, _SerializeConfig config) { var children = new List(); foreach (var child in node.getChildren()) { if (!config.summaryTree || this._shouldShowInSummaryTree(child)) { children.Add(child); } else { children.AddRange(this._getChildrenFiltered(child, config)); } } return children; } bool _shouldShowInSummaryTree(DiagnosticsNode node) { var value = node.valueObject; if (!(value is Diagnosticable)) { return true; } if (!(value is Element) || !this.isWidgetCreationTracked()) { return true; } return this._isValueCreatedByLocalProject(value); } string toId(object obj, string groupName) { if (obj == null) { return null; } HashSet<_InspectorReferenceData> group; this._groups.TryGetValue(groupName, out group); if (group == null) { group = new HashSet<_InspectorReferenceData>(); this._groups.Add(groupName, group); } string id; this._objectToId.TryGetValue(obj, out id); _InspectorReferenceData referenceData; if (id == null) { id = $"inspector-{this._nextId}"; this._nextId++; this._objectToId[obj] = id; referenceData = new _InspectorReferenceData(obj); this._idToReferenceData[id] = referenceData; group.Add(referenceData); } else { referenceData = this._idToReferenceData[id]; if (group.Add(referenceData)) { referenceData.count += 1; } } return id; } object toObject(string id) { if (id == null) { return null; } _InspectorReferenceData data; this._idToReferenceData.TryGetValue(id, out data); if (data == null) { throw new UIWidgetsError("Id does not exist."); } return data.obj; } bool isWidgetCreationTracked() { return false; //todo } bool _isValueCreatedByLocalProject(object value) { return true; // todo } void _decrementReferenceCount(_InspectorReferenceData reference) { reference.count -= 1; D.assert(reference.count >= 0); if (reference.count == 0) { string id; this._objectToId.TryGetValue(reference.obj, out id); D.assert(id != null); this._objectToId.Remove(reference.obj); this._idToReferenceData.Remove(id); } } } public class WidgetInspector : StatefulWidget { public readonly Widget child; public WidgetInspector(Key key, Widget child, InspectorSelectButtonBuilder selectButtonBuilder) : base(key) { D.assert(child != null); this.child = child; this.selectButtonBuilder = selectButtonBuilder; } public readonly InspectorSelectButtonBuilder selectButtonBuilder; public override State createState() { return new _WidgetInspectorState(WidgetsBinding.instance.widgetInspectorService.selection); } } class _WidgetInspectorState : State, WidgetsBindingObserver { public _WidgetInspectorState(InspectorSelection selection) { this.selection = selection; } Offset _lastPointerLocation; public readonly InspectorSelection selection; public bool isSelectMode = true; readonly GlobalKey _ignorePointerKey = GlobalKey.key(); const float _edgeHitMargin = 2.0f; const float _kOffScreenMargin = 1.0f; const float _kScreenEdgeMargin = 10.0f; const float _kTooltipPadding = 5.0f; const float _kInspectButtonMargin = 10.0f; public override void initState() { base.initState(); WidgetInspectorService.instance.selectionChangedCallback += this._selectionChangedCallback; } public override void dispose() { WidgetInspectorService.instance.selectionChangedCallback -= this._selectionChangedCallback; base.dispose(); } bool _hitTestHelper( List hits, List edgeHits, Offset position, RenderObject renderObject, Matrix3 transform ) { var hit = false; var inverse = Matrix3.I(); var invertible = transform.invert(inverse); if (!invertible) { return false; } var localPosition = inverse.mapPoint(position); List children = renderObject.debugDescribeChildren(); for (int i = children.Count - 1; i >= 0; --i) { DiagnosticsNode diagnostics = children[i]; D.assert(diagnostics != null); if (diagnostics.style == DiagnosticsTreeStyle.offstage || (!(diagnostics.valueObject is RenderObject))) { continue; } RenderObject child = (RenderObject) diagnostics.valueObject; Rect paintClip = renderObject.describeApproximatePaintClip(child); if (paintClip != null && !paintClip.contains(localPosition)) { continue; } var childTransform = new Matrix3(transform); renderObject.applyPaintTransform(child, childTransform); if (this._hitTestHelper(hits, edgeHits, position, child, childTransform)) { hit = true; } } Rect bounds = renderObject.semanticBounds; if (bounds.contains(localPosition)) { hit = true; if (!bounds.deflate(_edgeHitMargin).contains(localPosition)) { edgeHits.Add(renderObject); } } if (hit) { hits.Add(renderObject); } return hit; } List hitTest(Offset position, RenderObject root) { List regularHits = new List(); List edgeHits = new List(); this._hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null)); regularHits.Sort(CompareByArea); HashSet hits = new HashSet(edgeHits); foreach (var obj in regularHits) { if (!hits.Contains(obj)) { hits.Add(obj); edgeHits.Add(obj); } } return edgeHits; } void _inspectAt(Offset position) { if (!this.isSelectMode) { return; } RenderIgnorePointer ignorePointer = (RenderIgnorePointer) this._ignorePointerKey.currentContext.findRenderObject(); RenderObject userRender = ignorePointer.child; List selected = this.hitTest(position, userRender); this.setState(() => { this.selection.candidates = selected; }); } void _handlePanDown(DragDownDetails evt) { this._lastPointerLocation = evt.globalPosition; this._inspectAt(evt.globalPosition); } void _handlePanUpdate(DragUpdateDetails evt) { this._lastPointerLocation = evt.globalPosition; this._inspectAt(evt.globalPosition); } void _handlePanEnd(DragEndDetails details) { Rect bounds = (Offset.zero & (Window.instance.physicalSize / Window.instance.devicePixelRatio)).deflate( _kOffScreenMargin); if (!bounds.contains(this._lastPointerLocation)) { this.setState(() => { this.selection.clear(); }); } } void _handleTap() { if (!this.isSelectMode) { return; } if (this._lastPointerLocation != null) { this._inspectAt(this._lastPointerLocation); if (this.selection != null) { if (WidgetInspectorService.instance.developerInspect != null) { WidgetInspectorService.instance.developerInspect(); } } } this.setState(() => { if (this.widget.selectButtonBuilder != null) { this.isSelectMode = false; } }); } void _handleEnableSelect() { this.setState(() => { this.isSelectMode = true; }); } public override Widget build(BuildContext context) { List children = new List(); children.Add(new GestureDetector( onTap: this._handleTap, onPanDown: this._handlePanDown, onPanEnd: this._handlePanEnd, onPanUpdate: this._handlePanUpdate, behavior: HitTestBehavior.opaque, child: new IgnorePointer( ignoring: this.isSelectMode, key: this._ignorePointerKey, child: this.widget.child ) )); if (!this.isSelectMode && this.widget.selectButtonBuilder != null) { children.Add(new Positioned( left: _kInspectButtonMargin, bottom: _kInspectButtonMargin, child: this.widget.selectButtonBuilder(context, this._handleEnableSelect) )); } children.Add(new _InspectorOverlay(null, this.selection)); return new Stack(children: children); } public void didChangeMetrics() { } public void didChangeTextScaleFactor() { } public void didChangeLocales(List locale) { } public IPromise didPopRoute() { return Promise.Resolved(false); } public IPromise didPushRoute(string route) { return Promise.Resolved(false); } void _selectionChangedCallback() { this.setState(() => { }); } static int CompareByArea(RenderObject o1, RenderObject o2) { return _area(o1).CompareTo(_area(o2)); } static float _area(RenderObject obj) { var bounds = obj.semanticBounds; Size size = null; if (bounds != null) { size = bounds.size; } return size == null ? float.PositiveInfinity : size.width * size.height; } } public class InspectorSelection { RenderObject _current; Element _currentElement; List _candidates = new List(); public List candidates { get { return this._candidates; } set { this._candidates = value; this._index = 0; this._computeCurrent(); } } int _index = 0; public int index { get { return this._index; } set { this._index = value; this._computeCurrent(); } } public void clear() { this.candidates = new List(); } public RenderObject current { get { return this._current; } set { if (this._current != value) { this._current = value; _DebugCreator creator = value.debugCreator as _DebugCreator; this._currentElement = creator.element; } } } public Element currentElement { get { return this._currentElement; } set { if (!ReferenceEquals(this.currentElement, value)) { this._currentElement = value; this._current = value.findRenderObject(); } } } void _computeCurrent() { if (this._index < this.candidates.Count) { this._current = this.candidates[this.index]; this._currentElement = ((_DebugCreator) this._current.debugCreator).element; } else { this._current = null; this._currentElement = null; } } public bool active { get { return this._current != null && this._current.attached; } } } class _InspectorOverlay : LeafRenderObjectWidget { public _InspectorOverlay(Key key, InspectorSelection selection) : base(key) { this.selection = selection; } public readonly InspectorSelection selection; public override RenderObject createRenderObject(BuildContext context) { return new _RenderInspectorOverlay(this.selection); } public override void updateRenderObject(BuildContext context, RenderObject renderObject) { ((_RenderInspectorOverlay) renderObject).selection = this.selection; } } class _RenderInspectorOverlay : RenderBox { public _RenderInspectorOverlay(InspectorSelection selection) { D.assert(selection != null); this._selection = selection; } InspectorSelection _selection; public InspectorSelection selection { get { return this._selection; } set { if (value != this._selection) { this._selection = value; this.markNeedsPaint(); } } } protected override bool sizedByParent { get { return true; } } protected override bool alwaysNeedsCompositing { get { return true; } } protected override void performResize() { this.size = this.constraints.constrain(new Size(float.PositiveInfinity, float.PositiveInfinity)); } public override void paint(PaintingContext context, Offset offset) { D.assert(this.needsCompositing); context.addLayer(new _InspectorOverlayLayer( Rect.fromLTWH(offset.dx, offset.dy, this.size.width, this.size.height), this.selection )); } } class _TransformedRect : IEquatable<_TransformedRect> { public readonly Rect rect; public readonly Matrix3 transform; public _TransformedRect(RenderObject obj) { this.rect = obj.semanticBounds; this.transform = obj.getTransformTo(null); } public bool Equals(_TransformedRect other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(this.rect, other.rect) && this.transform.Equals(other.transform); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != this.GetType()) { return false; } return this.Equals((_TransformedRect) obj); } public override int GetHashCode() { unchecked { return ((this.rect != null ? this.rect.GetHashCode() : 0) * 397) ^ this.transform.GetHashCode(); } } public static bool operator ==(_TransformedRect left, _TransformedRect right) { return Equals(left, right); } public static bool operator !=(_TransformedRect left, _TransformedRect right) { return !Equals(left, right); } } class _InspectorOverlayRenderState : IEquatable<_InspectorOverlayRenderState> { public readonly Rect overlayRect; public readonly _TransformedRect selected; public readonly List<_TransformedRect> candidates; public readonly string tooltip; public readonly TextDirection textDirection; public _InspectorOverlayRenderState(Rect overlayRect, _TransformedRect selected, List<_TransformedRect> candidates, string tooltip, TextDirection textDirection) { this.overlayRect = overlayRect; this.selected = selected; this.candidates = candidates; this.tooltip = tooltip; this.textDirection = textDirection; } public bool Equals(_InspectorOverlayRenderState other) { if (ReferenceEquals(null, other)) { return false; } if (ReferenceEquals(this, other)) { return true; } return Equals(this.overlayRect, other.overlayRect) && Equals(this.selected, other.selected) && string.Equals(this.tooltip, other.tooltip) && this.textDirection == other.textDirection && (this.candidates == other.candidates || (this.candidates != null && this.candidates.SequenceEqual( other.candidates))); } public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != this.GetType()) { return false; } return this.Equals((_InspectorOverlayRenderState) obj); } public override int GetHashCode() { unchecked { var hashCode = (this.overlayRect != null ? this.overlayRect.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (this.selected != null ? this.selected.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (this.candidates != null ? this.candidates.Aggregate(0, (hash, rect) => (hash * 397) ^ (rect == null ? 0 : rect.GetHashCode())) : 0); hashCode = (hashCode * 397) ^ (this.tooltip != null ? this.tooltip.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (int) this.textDirection; return hashCode; } } public static bool operator ==(_InspectorOverlayRenderState left, _InspectorOverlayRenderState right) { return Equals(left, right); } public static bool operator !=(_InspectorOverlayRenderState left, _InspectorOverlayRenderState right) { return !Equals(left, right); } } class _InspectorOverlayLayer : Layer { // const int _kMaxTooltipLines = 5; // private static Color _kTooltipBackgroundColor = Color.fromARGB(230, 60, 60, 60); static Color _kHighlightedRenderObjectFillColor = Color.fromARGB(128, 128, 128, 255); static Color _kHighlightedRenderObjectBorderColor = Color.fromARGB(128, 64, 64, 128); public _InspectorOverlayLayer(Rect overlayRect, InspectorSelection selection) { this.overlayRect = overlayRect; this.selection = selection; } public InspectorSelection selection; public readonly Rect overlayRect; _InspectorOverlayRenderState _lastState; public Picture _picture; // public TextPainter _textPainter; // public float _textPainterMaxWidth; public override void addToScene(SceneBuilder builder, Offset layerOffset) { if (!this.selection.active) { return; } RenderObject selected = this.selection.current; List<_TransformedRect> candidates = new List<_TransformedRect>(); foreach (RenderObject candidate in this.selection.candidates) { if (candidate == selected || !candidate.attached) { continue; } candidates.Add(new _TransformedRect(candidate)); } _InspectorOverlayRenderState state = new _InspectorOverlayRenderState(this.overlayRect, new _TransformedRect(selected), candidates, this.selection.currentElement.toStringShort(), TextDirection.ltr ); if (state != this._lastState) { this._lastState = state; this._picture = this._buildPicture(state); } builder.addPicture(layerOffset, this._picture); } Picture _buildPicture(_InspectorOverlayRenderState state) { PictureRecorder recorder = new PictureRecorder(); Canvas canvas = new RecorderCanvas(recorder); Size size = state.overlayRect.size; var fillPaint = new Paint() {color = _kHighlightedRenderObjectFillColor}; var borderPaint = new Paint() { color = _kHighlightedRenderObjectBorderColor, style = PaintingStyle.stroke, strokeWidth = 1 }; Rect selectedPaintRect = state.selected.rect.deflate(0.5f); canvas.save(); canvas.setMatrix(state.selected.transform); canvas.drawRect(selectedPaintRect, fillPaint); canvas.drawRect(selectedPaintRect, borderPaint); canvas.restore(); foreach (var transformedRect in state.candidates) { canvas.save(); canvas.setMatrix(transformedRect.transform); canvas.drawRect(transformedRect.rect.deflate(0.5f), borderPaint); canvas.restore(); } // todo paint descipion return recorder.endRecording(); } } }