#if UNITY_EDITOR using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; using Object = UnityEngine.Object; namespace WingmanInspector { public class WingmanContainer { public enum ShortcutOperation { Nothing, ToggleComponent } private const string AllButtonName = "All"; private const float DragThreshold = 12f; private const float MiniMapMargin = 4f; private const float SearchCompListSpace = 4f; private const float RowHeight = 25f; private const float InspectorScrollBarWidth = 12.666666667f; private const float ToolBarButtonWidth = 30f; private const string InspectorListClassName = "unity-inspector-editors-list"; private const string InspectorScrolllassName = "unity-inspector-root-scrollview"; private const string InspectorNoMultiEditClassName = "unity-inspector-no-multi-edit-warning"; private const string MainWingmanName = "Wingman Main"; private const string SearchResultsName = "SearchResults"; private const double TimeAfterLastKeyPressToSearch = 0.15; private const string DragAndDropKey = "WingmansDragAndDrop"; public static GUIStyle BoldLabelStyle; public static float SearchBarHeight; public static WingmanPersistentData PersistentData; public static Texture TextureAtlas; public static Texture AllIcon; public static Texture XIcon; public static GUIStyle LeftToolBarGuiStyle; public static GUIContent CopyToolBarGuiContent; public static GUIStyle RightToolBarGuiStyle; public static GUIContent PasteToolBarGuiContent; private static readonly Vector2 iconSize = new(12, 12); private static readonly Vector2 toolBarIconSize = new(12, 12); public readonly EditorWindow InspectorWindow; private ShortcutOperation activeShortcutToPerform; private bool canStartDrag; private readonly Dictionary compFromIndex = new(); private bool dragHandlerSet; private int dragId; private VisualElement editorListVisual; private Vector2 initialDragMousePos; private AssetType inspectingAssetType; private Object inspectingObject; private readonly ScrollView inspectorScrollView; private bool inspectorWasLocked; private bool isDragging; public bool IsFocused; private int lastCompCount; private int lastRowCount; private readonly PropertyInfo lockedPropertyInfo; private IMGUIContainer miniMapGuiContainer; private Vector2 miniMapScrollPos; private readonly HashSet noMultiEditVisualElements = new(); private bool performSearchFlag; private IMGUIContainer pinnedDividerContainer; private IMGUIContainer pinnedHeaderContainer; private readonly List prevValidCompIds = new(); private int rangeModifierPivot; private List searchResults = new(); private IMGUIContainer searchResultsGuiContainer; private List selectedCompIds; private double timeOfLastSearchUpdate; private readonly List validCompIds = new(); public WingmanContainer(EditorWindow window) { InspectorWindow = window; lockedPropertyInfo = window.GetType().GetProperty("isLocked", BindingFlags.Public | BindingFlags.Instance); inspectorWasLocked = InspectorIsLocked(); inspectorScrollView = (ScrollView)InspectorWindow.rootVisualElement.Q(null, InspectorScrolllassName); SetContainerSelectionToObject(inspectorWasLocked ? PersistentData.GetRestoredObjectForInspectorWindow(window) : Selection.activeObject); } public void PerformShortcutOperation(ShortcutOperation shortcut) { activeShortcutToPerform = shortcut; // Force update, otherwise we wait for mouse movement to trigger gui handler miniMapGuiContainer?.MarkDirtyRepaint(); } public void RemoveGui() { if (!InspectingObjectIsValid()) return; if (ShowingWingmanGui()) editorListVisual?.RemoveAt(MiniMapIndex()); if (ShowingSearchResults()) editorListVisual?.RemoveAt(SearchResultsIndex()); } public void SetContainerSelectionToObject(Object obj) { inspectingObject = obj; if (!inspectingObject) { inspectingAssetType = AssetType.NotImportant; return; } // Figure out what type of asset we are inspecting { var isAsset = AssetDatabase.Contains(inspectingObject); var prefabType = PrefabUtility.GetPrefabAssetType(inspectingObject); if (isAsset && prefabType is PrefabAssetType.Regular or PrefabAssetType.Variant) inspectingAssetType = AssetType.ProjectPrefab; else if (!isAsset && prefabType is PrefabAssetType.Model) inspectingAssetType = AssetType.HierarchyModel; else if (!isAsset && prefabType is PrefabAssetType.Regular or PrefabAssetType.Variant) inspectingAssetType = AssetType.HierarchyPrefab; else if (!isAsset && prefabType is PrefabAssetType.NotAPrefab) inspectingAssetType = AssetType.HierarchyGameObject; else inspectingAssetType = AssetType.NotImportant; } searchResults.Clear(); RefreshNoMultiInspectVisualsSet(); PersistentData.AddDataForContainer(inspectingObject); selectedCompIds = PersistentData.SelectedCompIds(inspectingObject); if (HasTextInSearchField()) { PerformSearch(); if (!HasSearchResults()) PersistentData.SetSearchString(inspectingObject, string.Empty); } } public void Update() { CheckForLockStatusChange(); if (!InspectingObjectIsValid()) return; if (Settings.TransOnlyDisable && OnlyHasTransform()) return; editorListVisual ??= InspectorWindow.rootVisualElement.Q(null, InspectorListClassName); if (editorListVisual == null) return; if (performSearchFlag && EditorApplication.timeSinceStartup - timeOfLastSearchUpdate > TimeAfterLastKeyPressToSearch) { PerformSearch(); performSearchFlag = false; searchResultsGuiContainer?.MarkDirtyRepaint(); } if (!ShowingWingmanGui() && editorListVisual.childCount > MiniMapIndex()) { var miniMapHeight = CalculateMiniMapHeight(); miniMapGuiContainer = new IMGUIContainer(); miniMapGuiContainer.name = MainWingmanName; miniMapGuiContainer.style.width = FullLength(); miniMapGuiContainer.style.height = miniMapHeight; miniMapGuiContainer.style.minHeight = miniMapHeight; miniMapGuiContainer.onGUIHandler = DrawWingmanGui; Margin(miniMapGuiContainer.style, MiniMapMargin); editorListVisual.Insert(MiniMapIndex(), miniMapGuiContainer); UpdateComponentVisibility(); } var searchResultsAreStale = SearchResultsAreStale(); if (searchResultsAreStale) { PerformSearch(); searchResultsGuiContainer?.MarkDirtyRepaint(); } var showingSearchResults = ShowingSearchResults(); if (!showingSearchResults && HasSearchResults() && editorListVisual.childCount > SearchResultsIndex()) { searchResultsGuiContainer = new IMGUIContainer(); searchResultsGuiContainer.name = SearchResultsName; searchResultsGuiContainer.style.width = FullLength(); searchResultsGuiContainer.style.height = FullLength(); searchResultsGuiContainer.onGUIHandler = DrawSearchResultsGui; editorListVisual.Insert(SearchResultsIndex(), searchResultsGuiContainer); searchResultsGuiContainer?.MarkDirtyRepaint(); } if (showingSearchResults && !HasSearchResults()) { RemoveSearchGui(); ToggleAllComonentVisibility(true); } #if UNITY_2021 Fix2021EditorMargins(); #endif } public void OnHierarchyGUI() { if (DragAndDrop.GetGenericData(DragAndDropKey) is not bool initiatedDrag || !initiatedDrag) return; if (Event.current.type == EventType.DragUpdated && !dragHandlerSet) { DragAndDrop.AddDropHandler(HierarchyDropHandler); dragHandlerSet = true; Event.current.Use(); } if (Event.current.type == EventType.DragExited && dragHandlerSet) { DragAndDrop.RemoveDropHandler(HierarchyDropHandler); dragHandlerSet = false; Event.current.Use(); } } private void DrawWingmanGui() { var reservedRect = miniMapGuiContainer.contentRect; IsFocused = reservedRect.Contains(Event.current.mousePosition); if (!InspectingObjectIsValid()) return; var showCopyPasteOnly = Settings.TransOnlyKeepCopyPaste && OnlyHasTransform(); if (!Settings.HideToolbar || showCopyPasteOnly) { DrawToolBar(reservedRect, showCopyPasteOnly); reservedRect = ShiftRectStartVertically(reservedRect, SearchBarHeight + SearchCompListSpace); } var comps = GetAllVisibleComponents(); var buttonWidths = GetButtonWidths(comps); var newCompCount = comps.Count; var newRowCount = GetRowCount(reservedRect.width, buttonWidths); // Create associated component data compFromIndex.Clear(); validCompIds.Clear(); for (var i = 0; i < comps.Count; i++) { compFromIndex.Add(i, comps[i]); validCompIds.Add(comps[i].GetInstanceID()); } // Check for resizing the container var resizeRequired = newCompCount != lastCompCount || newRowCount != lastRowCount; if (resizeRequired) ResizeGuiContainer(); // Remove component from selection if it was removed from gameobject if (newCompCount < lastCompCount) for (var i = selectedCompIds.Count - 1; i >= 0; i--) if (!validCompIds.Contains(selectedCompIds[i])) selectedCompIds.RemoveAt(i); var compsGotAdjusted = newCompCount < lastCompCount || !CompareComponentIds(validCompIds, prevValidCompIds); // Set variables for next method call prevValidCompIds.Clear(); foreach (var validCompId in validCompIds) prevValidCompIds.Add(validCompId); lastCompCount = newCompCount; lastRowCount = newRowCount; GetScrollViewDimensions(reservedRect, newRowCount, out var innerScrollRect, out var outerScrollRect); var buttonPlacements = GetButtonPlacements(innerScrollRect, comps, buttonWidths); CheckToShowContextMenu(comps, buttonPlacements); CheckForShortcutOperations(comps, buttonPlacements); if (showCopyPasteOnly) return; UpdateDragAndDrop(); EditorGUI.BeginChangeCheck(); DrawPreviewScrollView(buttonPlacements, comps, innerScrollRect, outerScrollRect); if (EditorGUI.EndChangeCheck() || compsGotAdjusted) UpdateComponentVisibility(); } private void DrawPreviewScrollView(List placementRects, List comps, Rect innerScrollRect, Rect outerScrollRect) { miniMapScrollPos = GUI.BeginScrollView(outerScrollRect, miniMapScrollPos, innerScrollRect, GUIStyle.none, GUIStyle.none); // Handle the All button { const int allButtonId = -1; var prevAllButtonToggle = AllIsSelected() && !HasTextInSearchField(); var allButtonRect = placementRects[0]; if (allButtonRect.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseDown) { canStartDrag = true; dragId = allButtonId; ClearSearchOnComponentButtonPress(); } var draggingAll = dragId == allButtonId && !prevAllButtonToggle; if (DrawToggleButton(allButtonRect, AllIcon, AllButtonName, prevAllButtonToggle, true, draggingAll)) { selectedCompIds.Clear(); rangeModifierPivot = 0; } } var modifiers = Event.current.modifiers; var multiSelectModifier = modifiers.HasFlag(EventModifiers.Control); var rangeSelectModifier = modifiers.HasFlag(EventModifiers.Shift); for (var i = 0; i < comps.Count; i++) { var comp = comps[i]; var buttonRect = placementRects[i + 1]; var compId = comp.GetInstanceID(); if (buttonRect.Contains(Event.current.mousePosition)) if (Event.current.type == EventType.MouseDown && Event.current.button == 0) { canStartDrag = true; dragId = compId; } var compName = comp.GetType().Name; var content = EditorGUIUtility.ObjectContent(comp, comp.GetType()); var displayCompAsEnabled = true; if (ComponentIsTogglable(comp)) displayCompAsEnabled = GetComponentEnabledState(comp); var prevToggle = selectedCompIds.Contains(compId); var draggingButton = compId == dragId && !prevToggle; var toggled = DrawToggleButton(buttonRect, content.image, compName, prevToggle, displayCompAsEnabled, draggingButton); if (toggled && !prevToggle) { OnButtonToggleOn(i, multiSelectModifier, rangeSelectModifier); ClearSearchOnComponentButtonPress(); } else if (!toggled && prevToggle) { OnButtonToggleOff(i, multiSelectModifier, rangeSelectModifier); ClearSearchOnComponentButtonPress(); } } GUI.EndScrollView(); } private void GetScrollViewDimensions(Rect reservedRect, int rowCount, out Rect innerScrollRect, out Rect outerScrollRect) { innerScrollRect = new Rect(reservedRect) { height = rowCount * RowHeight }; outerScrollRect = new Rect(reservedRect) { height = RowHeight * Settings.MaxNumberOfRows }; } private List GetButtonPlacements(Rect scrollViewRect, List comps, float[] buttonWidths) { var placements = new List(); var placementRect = scrollViewRect; var usableWidth = scrollViewRect.width; if (!ShowingVerticalScrollBar()) usableWidth -= InspectorScrollBarWidth; var allButtonRect = new Rect(placementRect.position, new Vector2(buttonWidths[0], RowHeight)); placements.Add(allButtonRect); var curWidth = usableWidth; curWidth -= buttonWidths[0]; placementRect.position += new Vector2(buttonWidths[0], 0f); for (var i = 0; i < comps.Count; i++) { var buttonWidth = buttonWidths[i + 1]; if (curWidth < buttonWidth) { placementRect.position = new Vector2(scrollViewRect.position.x, placementRect.position.y + RowHeight); curWidth = usableWidth; } curWidth -= buttonWidth; var buttonRect = new Rect(placementRect.position, new Vector2(buttonWidth, RowHeight)); placements.Add(buttonRect); placementRect.position += new Vector2(buttonWidth, 0f); } return placements; } private void ClearSearchOnComponentButtonPress() { if (HasTextInSearchField()) { PersistentData.SetSearchString(inspectingObject, string.Empty); searchResults.Clear(); GUI.changed = true; RemoveSearchGui(); ToggleAllComonentVisibility(true); } } private bool DrawToggleButton(Rect placement, Texture icon, string label, bool toggled, bool compEnabled, bool beingDragged) { if (!toggled && isDragging && beingDragged) { toggled = true; GUI.changed = true; } else if (Event.current.type == EventType.MouseUp && placement.Contains(Event.current.mousePosition) && Event.current.button == 0) { toggled = !toggled; } var style = GUI.skin.button; var restoreGuiColor = GUI.color; if (!compEnabled) { var dimColor = new Color(0.67f, 0.67f, 0.67f, 1f); GUI.color = dimColor; // This tints everything drawn next } var uniqueControlId = GUIUtility.GetControlID(FocusType.Passive); GUI.Toggle(placement, uniqueControlId, toggled, GUIContent.none, style); GUI.color = restoreGuiColor; var iconPos = new Vector2(placement.position.x + BoldLabelStyle.margin.right, 0f); var iconRect = CenterRectVertically(placement, new Rect(iconPos, iconSize)); GUI.DrawTexture(iconRect, icon); var labelSize = BoldLabelStyle.CalcSize(new GUIContent(label)); var labelPos = new Vector2(iconRect.xMax, 0f); var labelRect = new Rect(labelPos, labelSize); labelRect = CenterRectVertically(placement, labelRect); GUI.Label(labelRect, label, BoldLabelStyle); return toggled; } private void OnButtonToggleOn(int compIndex, bool multiSelectModifier, bool rangeSelectModifier) { var compId = ComponentIdFromIndex(compIndex); if (multiSelectModifier && !rangeSelectModifier) { rangeModifierPivot = compIndex; selectedCompIds.Add(compId); return; } if (rangeSelectModifier) { if (AllIsSelected()) { rangeModifierPivot = compIndex; selectedCompIds.Add(compId); return; } AddRangeToSelected(compIndex); return; } selectedCompIds.Clear(); selectedCompIds.Add(compId); rangeModifierPivot = compIndex; } private void OnButtonToggleOff(int compIndex, bool multiSelectModifier, bool rangeSelectModifier) { var compId = ComponentIdFromIndex(compIndex); if (rangeSelectModifier && selectedCompIds.Count <= 1) return; if (!multiSelectModifier && !rangeSelectModifier && selectedCompIds.Count > 1) { selectedCompIds.Clear(); selectedCompIds.Add(compId); rangeModifierPivot = compIndex; return; } if (rangeSelectModifier) { if (compIndex == rangeModifierPivot) { selectedCompIds.Clear(); selectedCompIds.Add(compId); return; } AddRangeToSelected(compIndex); if (compIndex < rangeModifierPivot) { var islandMin = compIndex; while (selectedCompIds.Contains(ComponentIdFromIndex(islandMin - 1))) islandMin -= 1; for (var i = islandMin; i < compIndex; i++) selectedCompIds.Remove(ComponentIdFromIndex(i)); } else { var islandMax = compIndex; while (selectedCompIds.Contains(ComponentIdFromIndex(islandMax + 1))) islandMax += 1; for (var i = compIndex + 1; i <= islandMax; i++) selectedCompIds.Remove(ComponentIdFromIndex(i)); } return; } selectedCompIds.Remove(compId); } private void AddRangeToSelected(int compIndex) { var (min, max) = rangeModifierPivot < compIndex ? (rangeModifierPivot, compIndex) : (compIndex, rangeModifierPivot); for (var i = min; i <= max; i++) { var id = ComponentIdFromIndex(i); if (!selectedCompIds.Contains(id)) selectedCompIds.Add(id); } } private void DrawToolBar(Rect placementRect, bool showCopyPasteOnly) { placementRect.height = SearchBarHeight; var fullWidth = placementRect.width; var xStartPos = placementRect.position.x; if (!Settings.HideCopyPaste || showCopyPasteOnly) { if (DrawToolBarButton(placementRect, true)) CopySelectedToClipboard(); placementRect.position += new Vector2(ToolBarButtonWidth, 0f); if (DrawToolBarButton(placementRect, false)) PasteFromClipboard(); placementRect.position += new Vector2(ToolBarButtonWidth + MiniMapMargin, 0f); } if (showCopyPasteOnly) return; placementRect.width = fullWidth - (placementRect.position.x - xStartPos); const float crossSize = 11; const float crossDistFromEndOfSearch = 16; var crossPlacement = placementRect; crossPlacement.width = crossSize; crossPlacement.height = crossSize; crossPlacement.position = new Vector2(placementRect.xMax - crossDistFromEndOfSearch, placementRect.position.y); crossPlacement = CenterRectVertically(placementRect, crossPlacement); // Handle X input before drawing search field because it eats the input of overlayed elements var searchText = PersistentData.SearchString(inspectingObject); var showX = searchText != string.Empty; var pressedX = false; if (showX) if (crossPlacement.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseUp) { searchText = string.Empty; searchResults.Clear(); pressedX = true; } var prevSearchLen = searchText.Length; GUI.SetNextControlName("SearchField"); searchText = GUI.TextField(placementRect, searchText, EditorStyles.toolbarSearchField); // Deselect any selected components when typing in search if (!string.IsNullOrWhiteSpace(searchText)) selectedCompIds.Clear(); // If we click outside of the search bar unfocus it if (pressedX || (!placementRect.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseDown)) { GUI.FocusControl(null); if (string.IsNullOrWhiteSpace(searchText)) searchText = string.Empty; } // Draw X after search field so it shows on top if (showX) { var prevColor = GUI.color; GUI.color = new Vector4(prevColor.r, prevColor.g, prevColor.b, 0.7f); GUI.Button(crossPlacement, XIcon, GUIStyle.none); GUI.color = prevColor; } if (prevSearchLen != searchText.Length) { performSearchFlag = true; timeOfLastSearchUpdate = EditorApplication.timeSinceStartup; } PersistentData.SetSearchString(inspectingObject, searchText); } private bool DrawToolBarButton(Rect placement, bool copy) { placement.width = ToolBarButtonWidth; var pressed = GUI.Button(placement, copy ? CopyToolBarGuiContent : PasteToolBarGuiContent, copy ? LeftToolBarGuiStyle : RightToolBarGuiStyle); var iconRect = placement; iconRect.size = toolBarIconSize; iconRect = CenterRectVertically(placement, iconRect); iconRect = CenterRectHorizonally(placement, iconRect); if (EditorGUIUtility.isProSkin) { var uvRect = copy ? new Rect(0f, 0.5f, 0.5f, 0.5f) : new Rect(0f, 0f, 0.5f, 0.5f); GUI.DrawTextureWithTexCoords(iconRect, TextureAtlas, uvRect); } else { var uvRect = copy ? new Rect(0.5f, 0.5f, 0.5f, 0.5f) : new Rect(0.5f, 0f, 0.5f, 0.5f); GUI.DrawTextureWithTexCoords(iconRect, TextureAtlas, uvRect); } return pressed; } private List GetComponentsFromSelection() { if (!InspectingObjectIsValid()) return null; var allComps = GetAllVisibleComponents(); if (AllIsSelected()) return allComps; var selComps = new List(selectedCompIds.Count); foreach (var compId in selectedCompIds) selComps.Add(ComponentFromId(compId)); return selComps; } private void PerformSearch() { var searchText = PersistentData.SearchString(inspectingObject); if (string.IsNullOrWhiteSpace(searchText)) { searchResults.Clear(); return; } var comps = GetAllVisibleComponents(); if (comps == null) return; searchResults.Clear(); foreach (var comp in comps) { ComponentSearchResults results = null; var serializedComponent = new SerializedObject(comp); var fields = GetComponentFields(serializedComponent); if (fields == null) continue; foreach (var field in fields) if (FuzzyMatch(field.displayName, searchText)) { searchResults ??= new List(); results ??= new ComponentSearchResults { Comp = comp, SerializedComponent = serializedComponent }; results.Fields.Add(field); } if (results != null) searchResults.Add(results); } } private bool FuzzyMatch(string stringToSearch, string pattern) { const int adjacencyBonus = 5; const int separatorBonus = 10; const int camelBonus = 10; const int leadingLetterPenalty = -5; const int maxLeadingLetterPenalty = -9; const int unmatchedLetterPenalty = -1; var score = 0; var patternIdx = 0; var patternLength = pattern.Length; var strIdx = 0; var strLength = stringToSearch.Length; var prevMatched = false; var prevLower = false; var prevSeparator = true; char? bestLetter = null; char? bestLower = null; var bestLetterScore = 0; while (strIdx != strLength) { var patternChar = patternIdx != patternLength ? pattern[patternIdx] as char? : null; var strChar = stringToSearch[strIdx]; var patternLower = patternChar != null ? char.ToLower((char)patternChar) as char? : null; var strLower = char.ToLower(strChar); var strUpper = char.ToUpper(strChar); var nextMatch = patternChar != null && patternLower == strLower; var rematch = bestLetter != null && bestLower == strLower; var advanced = nextMatch && bestLetter != null; var patternRepeat = bestLetter != null && patternChar != null && bestLower == patternLower; if (advanced || patternRepeat) { score += bestLetterScore; bestLetter = null; bestLower = null; bestLetterScore = 0; } if (nextMatch || rematch) { var newScore = 0; if (patternIdx == 0) { var penalty = Math.Max(strIdx * leadingLetterPenalty, maxLeadingLetterPenalty); score += penalty; } if (prevMatched) newScore += adjacencyBonus; if (prevSeparator) newScore += separatorBonus; if (prevLower && strChar == strUpper && strLower != strUpper) newScore += camelBonus; if (nextMatch) ++patternIdx; if (newScore >= bestLetterScore) { if (bestLetter != null) score += unmatchedLetterPenalty; bestLetter = strChar; bestLower = char.ToLower((char)bestLetter); bestLetterScore = newScore; } prevMatched = true; } else { score += unmatchedLetterPenalty; prevMatched = false; } prevLower = strChar == strLower && strLower != strUpper; prevSeparator = strChar == '_' || strChar == ' '; ++strIdx; } if (bestLetter != null) score += bestLetterScore; const int idealScore = -10; return patternIdx == patternLength && score >= idealScore; } private DragAndDropVisualMode HierarchyDropHandler(int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform) { const int hierarchyId = -1314; var copying = dropMode == HierarchyDropFlags.DropUpon && dropTargetInstanceID != hierarchyId; var creating = dropTargetInstanceID == hierarchyId || dropMode == HierarchyDropFlags.DropBetween || dropMode == HierarchyDropFlags.None; var visualMode = DragAndDropVisualMode.None; if (copying) visualMode = DragAndDropVisualMode.Copy; else if (creating) visualMode = DragAndDropVisualMode.Move; if (!perform || (!copying && !creating)) return visualMode; var comps = GetComponentsFromSelection(); if (comps == null) return visualMode; if (copying && EditorUtility.InstanceIDToObject(dropTargetInstanceID) is GameObject gameObject) { GroupUndoAction("Copy Components", () => gameObject.PasteComponents(comps)); EditorApplication.delayCall += () => Selection.activeObject = gameObject; return visualMode; } GroupUndoAction("Create Object from Components", () => { var newGameObject = new GameObject("GameObject"); Undo.RegisterCreatedObjectUndo(newGameObject, string.Empty); newGameObject.PasteComponentsFromEmpty(comps); EditorApplication.delayCall += () => Selection.activeObject = newGameObject; }); return visualMode; } private void GroupUndoAction(string undoName, Action action) { Undo.IncrementCurrentGroup(); var curUndoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName(undoName); action.Invoke(); Undo.CollapseUndoOperations(curUndoGroup); } private void UpdateDragAndDrop() { var mouseDragEvent = Event.current.type == EventType.MouseDrag; if (!isDragging && canStartDrag && mouseDragEvent) { initialDragMousePos = Event.current.mousePosition; canStartDrag = false; return; } if (initialDragMousePos != Vector2.zero && mouseDragEvent && Vector2.Distance(initialDragMousePos, Event.current.mousePosition) >= DragThreshold) { DragAndDrop.PrepareStartDrag(); DragAndDrop.SetGenericData(DragAndDropKey, true); DragAndDrop.StartDrag(MainWingmanName); isDragging = true; } // DragExited is set when we drag out of the container or stop dragging inside it if (Event.current.type == EventType.DragExited) { canStartDrag = false; isDragging = false; initialDragMousePos = Vector2.zero; Event.current.Use(); } } private bool CompareComponentIds(List list0, List list1) { if (list0.Count != list1.Count) return false; for (var i = 0; i < list0.Count; i++) if (list0[i] != list1[i]) return false; return true; } private void ResizeGuiContainer() { var height = CalculateMiniMapHeight(); miniMapGuiContainer.style.height = height; miniMapGuiContainer.style.minHeight = height; miniMapGuiContainer.style.width = FullLength(); } private void DrawSearchResultsGui() { if (!HasSearchResults() || SearchResultsAreStale() || !InspectingObjectIsValid()) return; ToggleAllComonentVisibility(false); foreach (var result in searchResults) { EditorGUILayout.InspectorTitlebar(true, result.Comp, false); EditorGUI.indentLevel++; foreach (var property in result.Fields) { EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(property, true); if (EditorGUI.EndChangeCheck()) result.SerializedComponent.ApplyModifiedProperties(); } EditorGUI.indentLevel--; EditorGUILayout.Space(); } } private void UpdateComponentVisibility() { var startIndex = ComponentStartIndex(); var skipedCount = 0; for (var i = startIndex; i < editorListVisual.childCount; i++) { if (noMultiEditVisualElements.Contains(editorListVisual[i].name)) { skipedCount++; continue; } var compIndex = i - startIndex - skipedCount; if (compFromIndex.TryGetValue(compIndex, out var comp)) { var showComp = selectedCompIds.Count <= 0 || selectedCompIds.Contains(comp.GetInstanceID()); editorListVisual[i].style.display = showComp ? DisplayStyle.Flex : DisplayStyle.None; } } } private void ToggleAllComonentVisibility(bool show) { var startIndex = ShowingSearchResults() ? SearchResultsIndex() + 1 : MiniMapIndex() + 1; for (var i = startIndex; i < editorListVisual.childCount; i++) editorListVisual[i].style.display = show ? DisplayStyle.Flex : DisplayStyle.None; } private bool ShowingWingmanGui() { var insertIndex = MiniMapIndex(); if (insertIndex >= editorListVisual.childCount) return false; var duplicateContainer = editorListVisual.hierarchy.Children() .FirstOrDefault(child => child.name == MainWingmanName); if (duplicateContainer != null) { var inCorrectPosition = editorListVisual.hierarchy.IndexOf(duplicateContainer) == insertIndex; if (inCorrectPosition) return true; duplicateContainer.RemoveFromHierarchy(); return false; } var potentialMiniMap = editorListVisual.hierarchy.ElementAt(insertIndex); return potentialMiniMap != null && potentialMiniMap.name == MainWingmanName; } private bool ShowingSearchResults() { var insertIndex = SearchResultsIndex(); if (insertIndex >= editorListVisual.childCount) return false; var potentialSearchResults = editorListVisual.hierarchy.ElementAt(insertIndex); return potentialSearchResults != null && potentialSearchResults.name == SearchResultsName; } private bool HasSearchResults() { return searchResults != null && searchResults.Count > 0; } private bool SearchResultsAreStale() { return searchResults != null && searchResults.Count > 0 && !searchResults[0].Comp; } private bool OnlyHasTransform() { #if UNITY_6000_0_OR_NEWER return ((GameObject)inspectingObject).GetComponentCount() == 1; #else return ((GameObject)inspectingObject).GetComponents().Length == 1; #endif } private int GetRowCount(float rowWidth, float[] buttonWidths) { if (!ShowingVerticalScrollBar()) rowWidth -= InspectorScrollBarWidth; var rowCount = 1; var curWidth = rowWidth; foreach (var buttonWidth in buttonWidths) { if (curWidth < buttonWidth) { curWidth = rowWidth; rowCount++; } curWidth -= buttonWidth; } return rowCount; } private float[] GetButtonWidths(List comps) { var buttonWidths = new float[comps.Count + 1]; buttonWidths[0] = GetButtonWidth(AllButtonName); for (var i = 1; i < buttonWidths.Length; i++) buttonWidths[i] = GetButtonWidth(comps[i - 1].GetType().Name); return buttonWidths; } private float GetButtonWidth(string text) { var totalPadding = BoldLabelStyle.margin.right * 2f; var guiSize = BoldLabelStyle.CalcSize(new GUIContent(text)); return iconSize.x + guiSize.x + totalPadding; } private List GetComponentFields(SerializedObject serializedComponent) { var iter = serializedComponent.GetIterator(); if (iter == null || !iter.NextVisible(true)) return null; var fields = new List(); do { fields.Add(iter.Copy()); } while (iter.NextVisible(false)); return fields; } private Rect CenterRectVertically(Rect parent, Rect child) { var yDiff = parent.height - child.height; var yPos = parent.position.y + yDiff / 2f; child.position = new Vector2(child.position.x, yPos); return child; } private Rect CenterRectHorizonally(Rect parent, Rect child) { var xDiff = parent.width - child.width; var xPos = parent.position.x + xDiff / 2f; child.position = new Vector2(xPos, child.position.y); return child; } private void Margin(IStyle style, float margin) { style.marginTop = margin; style.marginBottom = margin; style.marginLeft = margin; style.marginRight = margin; } private bool ShowingVerticalScrollBar() { return inspectorScrollView.verticalScroller.resolvedStyle.display == DisplayStyle.Flex; } private List GetAllVisibleComponents() { if (!InspectingObjectIsValid()) return null; var selectedGameObject = inspectingObject as GameObject; if (Selection.gameObjects.Length == 1) return GetAllVisibleComponents(selectedGameObject); { // Get all visible components that each selected object shares var comps = GetAllVisibleComponents(selectedGameObject); if (InspectorIsLocked()) return comps; foreach (var otherGameObject in Selection.gameObjects) { if (otherGameObject == selectedGameObject) continue; var otherComps = GetAllVisibleComponents(otherGameObject); for (var i = comps.Count - 1; i >= 0; i--) if (!ComponentListContainsType(otherComps, comps[i].GetType())) comps.RemoveAt(i); } return comps; } } private bool ComponentListContainsType(List list, Type componentType) { foreach (var component in list) if (component.GetType() == componentType) return true; return false; } private List GetAllVisibleComponents(GameObject gameObject) { var comps = gameObject.GetComponents(); var res = new List(comps.Length); foreach (var comp in comps) if (ComponentIsVisible(comp)) res.Add(comp); return res; } private bool ComponentIsVisible(Component comp) { // Comp can be null if the associated script cannot be loaded return comp && !comp.hideFlags.HasFlag(HideFlags.HideInInspector) && !ComponentIsOnBanList(comp); } private bool ComponentIsOnBanList(Component comp) { return comp is ParticleSystemRenderer; } private int ComponentIdFromIndex(int index) { return compFromIndex[index].GetInstanceID(); } private Component ComponentFromId(int compId) { var index = 0; for (var i = 0; i < validCompIds.Count; i++) if (validCompIds[i] == compId) index = i; return compFromIndex[index]; } private bool AllIsSelected() { return selectedCompIds.Count == 0; } public bool InspectorIsLocked() { return (bool)lockedPropertyInfo.GetValue(InspectorWindow); } private void CheckForLockStatusChange() { var currentlyLocked = InspectorIsLocked(); var wasJustLocked = currentlyLocked && !inspectorWasLocked; if (wasJustLocked) PersistentData.SetDataForLockedInspector(InspectorWindow, inspectingObject); var wasJustUnlocked = !currentlyLocked && inspectorWasLocked; if (wasJustUnlocked && Selection.activeObject != inspectingObject) SetContainerSelectionToObject(Selection.activeObject); inspectorWasLocked = currentlyLocked; } private int MiniMapIndex() { return inspectingAssetType is AssetType.ProjectPrefab ? 2 : 1; } private int SearchResultsIndex() { return inspectingAssetType is AssetType.ProjectPrefab ? 3 : 2; } private int ComponentStartIndex() { return inspectingAssetType == AssetType.ProjectPrefab ? 3 : 2; } private void RemoveSearchGui() { if (ShowingSearchResults()) { editorListVisual.RemoveAt(SearchResultsIndex()); searchResultsGuiContainer = null; } } private bool HasTextInSearchField() { return !string.IsNullOrWhiteSpace(PersistentData.SearchString(inspectingObject)); } private float CalculateMiniMapHeight() { var searchBarAndPadding = SearchBarHeight + SearchCompListSpace; if (Settings.TransOnlyKeepCopyPaste && OnlyHasTransform()) return SearchBarHeight; var buttonWidths = GetButtonWidths(GetAllVisibleComponents()); // Important! Use editor list width as container width as MiniMap.layout // is not always as up to date as it should be (if it were just created). // This prevents the container from flickering when changing objects. var guiContainerWidth = editorListVisual.layout.width - MiniMapMargin * 2f; float rowCount = Mathf.Clamp(GetRowCount(guiContainerWidth, buttonWidths), 1, Settings.MaxNumberOfRows); return rowCount * RowHeight + (Settings.HideToolbar ? 0f : searchBarAndPadding); } private StyleLength FullLength() { return new StyleLength(StyleKeyword.Auto); } private bool InspectingObjectIsValid() { return inspectingObject && inspectingObject is GameObject && inspectingAssetType is not AssetType.NotImportant; } // Add all visual elements to the noMultiEditVisualElements set so we know which components are not // being displayed in the inspector when multi-inspecting is occurring. // During multi-inspecting the editor list may have non-shared (hidden) components inserted as children // that we need to skip over when updating component visibility to not throw off component indexing. // Any visual element after no-multi-edit warning tells us what is being hidden in the inspector. private void RefreshNoMultiInspectVisualsSet() { noMultiEditVisualElements.Clear(); if (Selection.gameObjects.Length <= 1 || editorListVisual == null) return; var noMultiEditIndex = editorListVisual.childCount; for (var i = 0; i < editorListVisual.childCount; i++) if (editorListVisual[i].ClassListContains(InspectorNoMultiEditClassName)) { noMultiEditIndex = i; break; } for (var i = noMultiEditIndex + 1; i < editorListVisual.childCount; i++) noMultiEditVisualElements.Add(editorListVisual[i].name); } private void CheckToShowContextMenu(List comps, List buttonRects) { var mouseDown = Event.current.type is EventType.MouseDown; var rightClicking = Event.current.button == 1; if (!mouseDown || !rightClicking) return; Event.current.Use(); // Eat event so right clicking doesn't toggle component var menu = new GenericMenu(); menu.AddItem(new GUIContent("Copy Selection"), false, CopySelectedToClipboard); menu.AddItem(new GUIContent("Paste Clipboard"), false, PasteFromClipboard); var compUnderCursor = GetComponentUnderCursor(comps, buttonRects); if (compUnderCursor) { menu.AddSeparator(""); var compName = compUnderCursor.GetType().Name; // Copy component menu.AddItem(new GUIContent($"Copy {compName}"), false, () => { PersistentData.Clipboard.CopyComponents(new List { compUnderCursor }); }); // Open component as script if (compUnderCursor is MonoBehaviour) menu.AddItem(new GUIContent($"Edit {compName} Script"), false, () => { var script = MonoScript.FromMonoBehaviour(compUnderCursor as MonoBehaviour); if (script) AssetDatabase.OpenAsset(script); }); // Remove component if (compUnderCursor is not Transform) { menu.AddSeparator(""); menu.AddItem(new GUIContent($"Remove {compName}"), false, () => { RemoveComponentTypeFromSelection(compUnderCursor.GetType()); }); } } menu.ShowAsContext(); } private Component GetComponentUnderCursor(List comps, List buttonRects) { for (var i = 1; i < buttonRects.Count; i++) if (buttonRects[i].Contains(Event.current.mousePosition + miniMapScrollPos)) return comps[i - 1]; return null; } private void RemoveComponentTypeFromSelection(Type compType) { GroupUndoAction("Remove Component", () => { foreach (var gameObject in Selection.gameObjects) if (gameObject.TryGetComponent(compType, out var component)) Undo.DestroyObjectImmediate(component); }); } private void CopySelectedToClipboard() { PersistentData.Clipboard.CopyComponents(GetComponentsFromSelection()); } private void PasteFromClipboard() { if (InspectorIsLocked()) { (inspectingObject as GameObject).PasteComponents(PersistentData.Clipboard.Copies); return; } foreach (var gameObject in Selection.gameObjects) gameObject.PasteComponents(PersistentData.Clipboard.Copies); } private void CheckForShortcutOperations(List comps, List buttonRects) { if (activeShortcutToPerform == ShortcutOperation.ToggleComponent) { var compUnderCursor = GetComponentUnderCursor(comps, buttonRects); if (compUnderCursor && ComponentIsTogglable(compUnderCursor)) ToggleComponent(compUnderCursor); } activeShortcutToPerform = ShortcutOperation.Nothing; } private bool ComponentIsTogglable(Component comp) { return comp is Behaviour or Renderer or Collider; } private bool GetComponentEnabledState(Component comp) { return comp switch { Behaviour b => b.enabled, Renderer r => r.enabled, Collider c => c.enabled, _ => true }; } private void ToggleComponent(Component comp) { _ = comp switch { Behaviour b => b.enabled = !b.enabled, Renderer r => r.enabled = !r.enabled, Collider c => c.enabled = !c.enabled, _ => false }; } private Rect ShiftRectStartVertically(Rect rect, float length) { rect.position += new Vector2(0f, length); rect.height -= length; return rect; } private void Fix2021EditorMargins() { bool ShowingTransform() { if (!InspectingObjectIsValid()) return false; var compStartIndex = ComponentStartIndex(); if (editorListVisual.childCount <= compStartIndex) return false; return editorListVisual[compStartIndex].style.display != DisplayStyle.None; } if (miniMapGuiContainer == null) return; if (ShowingTransform()) { const float transformHeaderMissingHeight = 7f; miniMapGuiContainer.style.marginTop = 0f; miniMapGuiContainer.style.marginBottom = transformHeaderMissingHeight + MiniMapMargin; } else { Margin(miniMapGuiContainer.style, MiniMapMargin); miniMapGuiContainer.style.marginTop = 0f; } } private enum AssetType { NotImportant, HierarchyGameObject, HierarchyPrefab, HierarchyModel, ProjectPrefab } private class ComponentSearchResults { public Component Comp; public readonly List Fields = new(); public SerializedObject SerializedComponent; } } } #endif