using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Reflex.Core; using Reflex.Extensions; using Reflex.Injectors; using Reflex.Resolvers; using UnityEditor; using UnityEditor.IMGUI.Controls; #if UNITY_6000_2_OR_NEWER using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; #endif using UnityEngine; using UnityEngine.SceneManagement; namespace Reflex.Editor.DebuggingWindow { public class ReflexDebuggerWindow : EditorWindow { private const string ContainerIcon = "PreMatCylinder"; // d_PrefabModel On Icon, "PreMatCylinder" private const string ResolverIcon = "d_NetworkAnimator Icon"; // "d_eyeDropper.Large", "AnimatorStateTransition Icon", "RelativeJoint2D Icon" private const string InstanceIcon = "d_Prefab Icon"; // "d_Prefab Icon", "d_Prefab On Icon" [NonSerialized] private bool _isInitialized; [SerializeField] private TreeViewState _treeViewState; // Serialized in the window layout file so it survives assembly reloading [SerializeField] private MultiColumnHeaderState _multiColumnHeaderState; private int _id = -1; private SearchField _searchField; private Vector2 _bindingStackTraceScrollPosition; private MultiColumnTreeView TreeView { get; set; } private Rect SearchBarRect => new Rect(20f, 10f, position.width - 40f, 20f); private Rect MultiColumnTreeViewRect => new Rect(20, 30, position.width - 40, position.height - 200); private void OnEnable() { SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneUnloaded -= OnSceneUnloaded; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; } private async void OnSceneLoaded(Scene scene, LoadSceneMode mode) { await Task.Yield(); Refresh(); } private async void OnSceneUnloaded(Scene scene) { await Task.Yield(); Refresh(); } private async void OnPlayModeStateChanged(PlayModeStateChange playModeStateChange) { await Task.Yield(); Refresh(); } private void InitIfNeeded() { if (!_isInitialized) { // Check if it already exists (deserialized from window layout file or scriptable object) if (_treeViewState == null) _treeViewState = new TreeViewState(); bool firstInit = _multiColumnHeaderState == null; var headerState = MultiColumnTreeView.CreateDefaultMultiColumnHeaderState(); if (MultiColumnHeaderState.CanOverwriteSerializedFields(_multiColumnHeaderState, headerState)) MultiColumnHeaderState.OverwriteSerializedFields(_multiColumnHeaderState, headerState); _multiColumnHeaderState = headerState; var multiColumnHeader = new MultiColumnHeader(headerState) { canSort = false, height = MultiColumnHeader.DefaultGUI.minimumHeight }; if (firstInit) multiColumnHeader.ResizeToFit(); var treeModel = new TreeModel(GetData()); TreeView = new MultiColumnTreeView(_treeViewState, multiColumnHeader, treeModel); TreeView.ExpandAll(); _searchField = new SearchField(); _searchField.downOrUpArrowKeyPressed += TreeView.SetFocusAndEnsureSelectedItem; _isInitialized = true; } } private static List<(IResolver, Type[])> BuildMatrix(Container container) { var resolvers = container.ResolversByContract.Values .SelectMany(r => r) .ToHashSet(); if (ReflexEditorSettings.ShowInternalBindings == false) { if (container.TryGetResolver(typeof(Container), out var containerResolver)) { resolvers.Remove(containerResolver); } } if (ReflexEditorSettings.ShowInheritedBindings == false && container.Parent != null) { var parentResolvers = container.Parent.ResolversByContract.Values .SelectMany(r => r) .Distinct(); foreach (var parentResolver in parentResolvers) { resolvers.Remove(parentResolver); } } return resolvers.Select(resolver => (resolver, GetContracts(resolver, container))).ToList(); } private static Type[] GetContracts(IResolver resolver, Container container) { var result = new List(); foreach (var pair in container.ResolversByContract) { if (pair.Value.Contains(resolver)) { result.Add(pair.Key); } } return result.ToArray(); } private void BuildDataRecursively(MyTreeElement parent, Container container) { if (container == null) { return; } var containerTreeElement = new MyTreeElement(container.Name, parent.Depth + 1, ++_id, ContainerIcon, () => string.Empty, Array.Empty(), string.Empty, container.GetDebugProperties().BuildCallsite, kind: string.Empty); parent.Children.Add(containerTreeElement); containerTreeElement.Parent = parent; foreach (var pair in BuildMatrix(container)) { var resolverTreeElement = new MyTreeElement( string.Join(", ", pair.Item2.Select(x => x.GetName())), // In this case Name is not used for rendering, but for searching containerTreeElement.Depth + 1, ++_id, ResolverIcon, () => pair.Item1.GetDebugProperties().Resolutions.ToString(), pair.Item2.Select(x => x.GetName()).OrderBy(x => x).ToArray(), pair.Item1.Lifetime.ToString(), pair.Item1.GetDebugProperties().BindingCallsite, kind: pair.Item1.GetType().Name.Replace("Singleton", string.Empty).Replace("Transient", string.Empty).Replace("Scoped", string.Empty).Replace("Resolver", string.Empty) ); foreach (var (instance, callsite) in pair.Item1.GetDebugProperties().Instances.Where(tuple => tuple.Item1.IsAlive).Select(tuple => (tuple.Item1.Target, tuple.Item2))) { var instanceTreeElement = new MyTreeElement( $"{instance.GetType().GetName()} ({SHA1.ShortHash(instance.GetHashCode())})", resolverTreeElement.Depth + 1, ++_id, InstanceIcon, () => string.Empty, Array.Empty(), string.Empty, callsite, string.Empty ); instanceTreeElement.SetParent(resolverTreeElement); } resolverTreeElement.SetParent(containerTreeElement); } foreach (var scopedContainer in container.Children) { BuildDataRecursively(containerTreeElement, scopedContainer); } } private IList GetData() { var root = new MyTreeElement("Root", -1, ++_id, ContainerIcon, () => string.Empty, Array.Empty(), string.Empty, null, string.Empty); foreach (var rootContainer in Container.RootContainers) { BuildDataRecursively(root, rootContainer); } var list = new List(); TreeElementUtility.TreeToList(root, list); return list; } private void OnGUI() { Repaint(); InitIfNeeded(); if (UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG")) { PresentDebuggerEnabled(); } else { PresentDebuggerDisabled(); } GUILayout.FlexibleSpace(); PresentStatusBar(); } private void Refresh(PlayModeStateChange _ = default) { _isInitialized = false; InitIfNeeded(); } private void SearchBar(Rect rect) { TreeView.searchString = _searchField.OnGUI(rect, TreeView.searchString); GUILayoutUtility.GetRect(rect.width, rect.height); } private void DoTreeView(Rect rect) { TreeView.OnGUI(rect); GUILayoutUtility.GetRect(rect.width, rect.height); } private static void PresentDebuggerDisabled() { GUILayout.FlexibleSpace(); GUILayout.Label("To start debugging, enable the Reflex Debug Mode in the status bar.", Styles.LabelHorizontallyCentered); GUILayout.Label("Keep in mind that enabling Reflex Debug Mode will impact performance.", Styles.LabelHorizontallyCentered); } private void PresentDebuggerEnabled() { SearchBar(SearchBarRect); DoTreeView(MultiColumnTreeViewRect); GUILayout.Space(16); using (new GUILayout.HorizontalScope()) { GUILayout.Space(16); using (new GUILayout.VerticalScope()) { _bindingStackTraceScrollPosition = GUILayout.BeginScrollView(_bindingStackTraceScrollPosition); PresentCallSite(); GUILayout.EndScrollView(); GUILayout.Space(16); } GUILayout.Space(16); } } private void PresentHierarchyFilters() { EditorGUI.BeginChangeCheck(); ReflexEditorSettings.ShowInternalBindings = GUILayout.Toggle(ReflexEditorSettings.ShowInternalBindings, "Show Internal Bindings "); ReflexEditorSettings.ShowInheritedBindings = GUILayout.Toggle(ReflexEditorSettings.ShowInheritedBindings, "Show Inherited Bindings "); if (EditorGUI.EndChangeCheck()) { Refresh(); } } private void PresentStatusBar() { using (new EditorGUILayout.HorizontalScope(Styles.AppToolbar)) { GUILayout.FlexibleSpace(); PresentHierarchyFilters(); var refreshIcon = EditorGUIUtility.IconContent("d_TreeEditor.Refresh"); refreshIcon.tooltip = "Forces Tree View to Refresh"; if (GUILayout.Button(refreshIcon, Styles.StatusBarIcon, GUILayout.Width(25))) { Refresh(); } var debuggerIcon = UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG") ? EditorGUIUtility.IconContent("d_DebuggerEnabled") : EditorGUIUtility.IconContent("d_DebuggerDisabled"); debuggerIcon.tooltip = UnityScriptingDefineSymbols.IsDefined("REFLEX_DEBUG") ? "Reflex Debugger Enabled" : "Reflex Debugger Disabled"; if (GUILayout.Button(debuggerIcon, Styles.StatusBarIcon, GUILayout.Width(25))) { UnityScriptingDefineSymbols.Toggle("REFLEX_DEBUG", EditorUserBuildSettings.selectedBuildTargetGroup); } } } private void PresentCallSite() { var selection = TreeView.GetSelection(); if (selection == null || selection.Count == 0) { return; } var item = TreeView.Find(selection.Single()); if (item == null || item.Callsite == null) { return; } foreach (var callSite in item.Callsite) { PresentStackFrame(callSite.ClassName, callSite.FunctionName, callSite.Path, callSite.Line); } } private static void PresentStackFrame(string className, string functionName, string path, int line) { using (new GUILayout.HorizontalScope()) { GUILayout.Label($"{className}:{functionName}() →", Styles.StackTrace); if (PresentLinkButton($"{path}:{line}")) { UnityEditorInternal.InternalEditorUtility.OpenFileAtLineExternal(path, line); } } } private static bool PresentLinkButton(string label, params GUILayoutOption[] options) { var position = GUILayoutUtility.GetRect(new GUIContent(label), Styles.Hyperlink, options); position.y -= 3; Handles.color = Styles.Hyperlink.normal.textColor; Handles.DrawLine(new Vector3(position.xMin + (float)EditorStyles.linkLabel.padding.left, position.yMax), new Vector3(position.xMax - (float)EditorStyles.linkLabel.padding.right, position.yMax)); Handles.color = Color.white; EditorGUIUtility.AddCursorRect(position, MouseCursor.Link); return GUI.Button(position, label, Styles.Hyperlink); } } }