#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 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 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 static Vector2 iconSize = new Vector2(12, 12); private static Vector2 toolBarIconSize = new Vector2(12, 12); public readonly EditorWindow InspectorWindow; public bool IsFocused; public enum ShortcutOperation { Nothing, ToggleComponent } private ShortcutOperation activeShortcutToPerform; private Object inspectingObject; private VisualElement editorListVisual; private IMGUIContainer miniMapGuiContainer; private IMGUIContainer pinnedHeaderContainer; private IMGUIContainer searchResultsGuiContainer; private IMGUIContainer pinnedDividerContainer; private ScrollView inspectorScrollView; private List selectedCompIds; private List validCompIds = new List(); private List prevValidCompIds = new List(); private Dictionary compFromIndex = new Dictionary(); private HashSet noMultiEditVisualElements = new HashSet(); private Vector2 miniMapScrollPos; private int lastCompCount; private int lastRowCount; private enum AssetType { NotImportant, HierarchyGameObject, HierarchyPrefab, HierarchyModel, ProjectPrefab } private AssetType inspectingAssetType; private List searchResults = new List(); private const double TimeAfterLastKeyPressToSearch = 0.15; private double timeOfLastSearchUpdate; private bool performSearchFlag; private bool inspectorWasLocked; private PropertyInfo lockedPropertyInfo; private int rangeModifierPivot; private const string DragAndDropKey = "WingmansDragAndDrop"; private bool isDragging; private bool dragHandlerSet; private bool canStartDrag; private int dragId; private Vector2 initialDragMousePos; 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 { bool isAsset = AssetDatabase.Contains(inspectingObject); PrefabAssetType 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()) { float 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(); } bool searchResultsAreStale = SearchResultsAreStale(); if (searchResultsAreStale) { PerformSearch(); searchResultsGuiContainer?.MarkDirtyRepaint(); } bool 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() { Rect reservedRect = miniMapGuiContainer.contentRect; IsFocused = reservedRect.Contains(Event.current.mousePosition); if (!InspectingObjectIsValid()) return; bool showCopyPasteOnly = Settings.TransOnlyKeepCopyPaste && OnlyHasTransform(); if (!Settings.HideToolbar || showCopyPasteOnly) { DrawToolBar(reservedRect, showCopyPasteOnly); reservedRect = ShiftRectStartVertically(reservedRect, SearchBarHeight + SearchCompListSpace); } List comps = GetAllVisibleComponents(); float[] buttonWidths = GetButtonWidths(comps); int newCompCount = comps.Count; int newRowCount = GetRowCount(reservedRect.width, buttonWidths); // Create associated component data compFromIndex.Clear(); validCompIds.Clear(); for (int i = 0; i < comps.Count; i++) { compFromIndex.Add(i, comps[i]); validCompIds.Add(comps[i].GetInstanceID()); } // Check for resizing the container bool resizeRequired = newCompCount != lastCompCount || newRowCount != lastRowCount; if (resizeRequired) { ResizeGuiContainer(); } // Remove component from selection if it was removed from gameobject if (newCompCount < lastCompCount) { for (int i = selectedCompIds.Count - 1; i >= 0; i--) { if (!validCompIds.Contains(selectedCompIds[i])) { selectedCompIds.RemoveAt(i); } } } bool compsGotAdjusted = newCompCount < lastCompCount || !CompareComponentIds(validCompIds, prevValidCompIds); // Set variables for next method call prevValidCompIds.Clear(); foreach (int validCompId in validCompIds) { prevValidCompIds.Add(validCompId); } lastCompCount = newCompCount; lastRowCount = newRowCount; GetScrollViewDimensions(reservedRect, newRowCount, out Rect innerScrollRect, out Rect outerScrollRect); List 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; bool prevAllButtonToggle = AllIsSelected() && !HasTextInSearchField(); Rect allButtonRect = placementRects[0]; if (allButtonRect.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseDown) { canStartDrag = true; dragId = allButtonId; ClearSearchOnComponentButtonPress(); } bool draggingAll = dragId == allButtonId && !prevAllButtonToggle; if (DrawToggleButton(allButtonRect, AllIcon, AllButtonName, prevAllButtonToggle, true, draggingAll)) { selectedCompIds.Clear(); rangeModifierPivot = 0; } } EventModifiers modifiers = Event.current.modifiers; bool multiSelectModifier = modifiers.HasFlag(EventModifiers.Control); bool rangeSelectModifier = modifiers.HasFlag(EventModifiers.Shift); for (int i = 0; i < comps.Count; i++) { Component comp = comps[i]; Rect buttonRect = placementRects[i + 1]; int compId = comp.GetInstanceID(); if (buttonRect.Contains(Event.current.mousePosition)) { if (Event.current.type == EventType.MouseDown && Event.current.button == 0) { canStartDrag = true; dragId = compId; } } string compName = comp.GetType().Name; GUIContent content = EditorGUIUtility.ObjectContent(comp, comp.GetType()); bool displayCompAsEnabled = true; if (ComponentIsTogglable(comp)) { displayCompAsEnabled = GetComponentEnabledState(comp); } bool prevToggle = selectedCompIds.Contains(compId); bool draggingButton = compId == dragId && !prevToggle; bool 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) { List placements = new List(); Rect placementRect = scrollViewRect; float usableWidth = scrollViewRect.width; if (!ShowingVerticalScrollBar()) { usableWidth -= InspectorScrollBarWidth; } Rect allButtonRect = new Rect(placementRect.position, new Vector2(buttonWidths[0], RowHeight)); placements.Add(allButtonRect); float curWidth = usableWidth; curWidth -= buttonWidths[0]; placementRect.position += new Vector2(buttonWidths[0], 0f); for (int i = 0; i < comps.Count; i++) { float buttonWidth = buttonWidths[i + 1]; if (curWidth < buttonWidth) { placementRect.position = new Vector2(scrollViewRect.position.x, placementRect.position.y + RowHeight); curWidth = usableWidth; } curWidth -= buttonWidth; Rect 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; } GUIStyle style = GUI.skin.button; Color restoreGuiColor = GUI.color; if (!compEnabled) { Color dimColor = new Color(0.67f, 0.67f, 0.67f, 1f); GUI.color = dimColor; // This tints everything drawn next } int uniqueControlId = GUIUtility.GetControlID(FocusType.Passive); GUI.Toggle(placement, uniqueControlId, toggled, GUIContent.none, style); GUI.color = restoreGuiColor; Vector2 iconPos = new Vector2(placement.position.x + BoldLabelStyle.margin.right, 0f); Rect iconRect = CenterRectVertically(placement, new(iconPos, iconSize)); GUI.DrawTexture(iconRect, icon); Vector2 labelSize = BoldLabelStyle.CalcSize(new GUIContent(label)); Vector2 labelPos = new Vector2(iconRect.xMax, 0f); Rect 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) { int 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) { int 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) { int islandMin = compIndex; while (selectedCompIds.Contains(ComponentIdFromIndex(islandMin - 1))) { islandMin -= 1; } for (int i = islandMin; i < compIndex; i++) { selectedCompIds.Remove(ComponentIdFromIndex(i)); } } else { int islandMax = compIndex; while (selectedCompIds.Contains(ComponentIdFromIndex(islandMax + 1))) { islandMax += 1; } for (int i = compIndex + 1; i <= islandMax; i++) { selectedCompIds.Remove(ComponentIdFromIndex(i)); } } return; } selectedCompIds.Remove(compId); } private void AddRangeToSelected(int compIndex) { (int min, int max) = rangeModifierPivot < compIndex ? (rangeModifierPivot, compIndex) : (compIndex, rangeModifierPivot); for (int i = min; i <= max; i++) { int id = ComponentIdFromIndex(i); if (!selectedCompIds.Contains(id)) { selectedCompIds.Add(id); } } } private void DrawToolBar(Rect placementRect, bool showCopyPasteOnly) { placementRect.height = SearchBarHeight; float fullWidth = placementRect.width; float 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; Rect 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 string searchText = PersistentData.SearchString(inspectingObject); bool showX = searchText != string.Empty; bool pressedX = false; if (showX) { if (crossPlacement.Contains(Event.current.mousePosition) && Event.current.type == EventType.MouseUp) { searchText = string.Empty; searchResults.Clear(); pressedX = true; } } int 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) { Color 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; bool pressed = GUI.Button(placement, copy ? CopyToolBarGuiContent : PasteToolBarGuiContent, copy ? LeftToolBarGuiStyle : RightToolBarGuiStyle); Rect iconRect = placement; iconRect.size = toolBarIconSize; iconRect = CenterRectVertically(placement, iconRect); iconRect = CenterRectHorizonally(placement, iconRect); if (EditorGUIUtility.isProSkin) { Rect 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 { Rect 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; } List allComps = GetAllVisibleComponents(); if (AllIsSelected()) { return allComps; } List selComps = new List(selectedCompIds.Count); foreach (int compId in selectedCompIds) { selComps.Add(ComponentFromId(compId)); } return selComps; } private class ComponentSearchResults { public Component Comp; public SerializedObject SerializedComponent; public List Fields = new List(); } private void PerformSearch() { string searchText = PersistentData.SearchString(inspectingObject); if (string.IsNullOrWhiteSpace(searchText)) { searchResults.Clear(); return; } List comps = GetAllVisibleComponents(); if (comps == null) return; searchResults.Clear(); foreach (Component comp in comps) { ComponentSearchResults results = null; SerializedObject serializedComponent = new SerializedObject(comp); List fields = GetComponentFields(serializedComponent); if (fields == null) continue; foreach (SerializedProperty field in fields) { if (FuzzyMatch(field.displayName, searchText)) { searchResults ??= new List(); results ??= new() { 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; int score = 0; int patternIdx = 0; int patternLength = pattern.Length; int strIdx = 0; int strLength = stringToSearch.Length; bool prevMatched = false; bool prevLower = false; bool prevSeparator = true; char? bestLetter = null; char? bestLower = null; int bestLetterScore = 0; while (strIdx != strLength) { char? patternChar = patternIdx != patternLength ? pattern[patternIdx] as char? : null; char strChar = stringToSearch[strIdx]; char? patternLower = patternChar != null ? char.ToLower((char)patternChar) as char? : null; char strLower = char.ToLower(strChar); char strUpper = char.ToUpper(strChar); bool nextMatch = patternChar != null && patternLower == strLower; bool rematch = bestLetter != null && bestLower == strLower; bool advanced = nextMatch && bestLetter != null; bool patternRepeat = bestLetter != null && patternChar != null && bestLower == patternLower; if (advanced || patternRepeat) { score += bestLetterScore; bestLetter = null; bestLower = null; bestLetterScore = 0; } if (nextMatch || rematch) { int newScore = 0; if (patternIdx == 0) { int 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; bool copying = dropMode == HierarchyDropFlags.DropUpon && dropTargetInstanceID != hierarchyId; bool creating = dropTargetInstanceID == hierarchyId || dropMode == HierarchyDropFlags.DropBetween || dropMode == HierarchyDropFlags.None; DragAndDropVisualMode visualMode = DragAndDropVisualMode.None; if (copying) { visualMode = DragAndDropVisualMode.Copy; } else if (creating) { visualMode = DragAndDropVisualMode.Move; } if (!perform || (!copying && !creating)) { return visualMode; } List 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", () => { GameObject 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(); int curUndoGroup = Undo.GetCurrentGroup(); Undo.SetCurrentGroupName(undoName); action.Invoke(); Undo.CollapseUndoOperations(curUndoGroup); } private void UpdateDragAndDrop() { bool 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 (int i = 0; i < list0.Count; i++) { if (list0[i] != list1[i]) { return false; } } return true; } private void ResizeGuiContainer() { float 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 (ComponentSearchResults result in searchResults) { EditorGUILayout.InspectorTitlebar(true, result.Comp, false); EditorGUI.indentLevel++; foreach (SerializedProperty property in result.Fields) { EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(property, true); if (EditorGUI.EndChangeCheck()) { result.SerializedComponent.ApplyModifiedProperties(); } } EditorGUI.indentLevel--; EditorGUILayout.Space(); } } private void UpdateComponentVisibility() { int startIndex = ComponentStartIndex(); int skipedCount = 0; for (int i = startIndex; i < editorListVisual.childCount; i++) { if (noMultiEditVisualElements.Contains(editorListVisual[i].name)) { skipedCount++; continue; } int compIndex = i - startIndex - skipedCount; if (compFromIndex.TryGetValue(compIndex, out Component comp)) { bool showComp = selectedCompIds.Count <= 0 || selectedCompIds.Contains(comp.GetInstanceID()); editorListVisual[i].style.display = showComp ? DisplayStyle.Flex : DisplayStyle.None; } } } private void ToggleAllComonentVisibility(bool show) { int startIndex = ShowingSearchResults() ? SearchResultsIndex() + 1 : MiniMapIndex() + 1; for (int i = startIndex; i < editorListVisual.childCount; i++) { editorListVisual[i].style.display = show ? DisplayStyle.Flex : DisplayStyle.None; } } private bool ShowingWingmanGui() { int insertIndex = MiniMapIndex(); if (insertIndex >= editorListVisual.childCount) { return false; } VisualElement duplicateContainer = editorListVisual.hierarchy.Children().FirstOrDefault(child => child.name == MainWingmanName); if (duplicateContainer != null) { bool inCorrectPosition = editorListVisual.hierarchy.IndexOf(duplicateContainer) == insertIndex; if (inCorrectPosition) { return true; } duplicateContainer.RemoveFromHierarchy(); return false; } VisualElement potentialMiniMap = editorListVisual.hierarchy.ElementAt(insertIndex); return potentialMiniMap != null && potentialMiniMap.name == MainWingmanName; } private bool ShowingSearchResults() { int insertIndex = SearchResultsIndex(); if (insertIndex >= editorListVisual.childCount) { return false; } VisualElement 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; } int rowCount = 1; float curWidth = rowWidth; foreach (float buttonWidth in buttonWidths) { if (curWidth < buttonWidth) { curWidth = rowWidth; rowCount++; } curWidth -= buttonWidth; } return rowCount; } private float[] GetButtonWidths(List comps) { float[] buttonWidths = new float[comps.Count + 1]; buttonWidths[0] = GetButtonWidth(AllButtonName); for (int i = 1; i < buttonWidths.Length; i++) { buttonWidths[i] = GetButtonWidth(comps[i - 1].GetType().Name); } return buttonWidths; } private float GetButtonWidth(string text) { float totalPadding = BoldLabelStyle.margin.right * 2f; Vector2 guiSize = BoldLabelStyle.CalcSize(new GUIContent(text)); return iconSize.x + guiSize.x + totalPadding; } private List GetComponentFields(SerializedObject serializedComponent) { SerializedProperty iter = serializedComponent.GetIterator(); if (iter == null || !iter.NextVisible(true)) { return null; } List fields = new List(); do { fields.Add(iter.Copy()); } while (iter.NextVisible(false)); return fields; } private Rect CenterRectVertically(Rect parent, Rect child) { float yDiff = parent.height - child.height; float yPos = parent.position.y + (yDiff / 2f); child.position = new Vector2(child.position.x, yPos); return child; } private Rect CenterRectHorizonally(Rect parent, Rect child) { float xDiff = parent.width - child.width; float 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; } GameObject selectedGameObject = inspectingObject as GameObject; if (Selection.gameObjects.Length == 1) { return GetAllVisibleComponents(selectedGameObject); } { // Get all visible components that each selected object shares List comps = GetAllVisibleComponents(selectedGameObject); if (InspectorIsLocked()) { return comps; } foreach (GameObject otherGameObject in Selection.gameObjects) { if (otherGameObject == selectedGameObject) continue; List otherComps = GetAllVisibleComponents(otherGameObject); for (int 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 (Component component in list) { if (component.GetType() == componentType) { return true; } } return false; } private List GetAllVisibleComponents(GameObject gameObject) { Component[] comps = gameObject.GetComponents(); List res = new List(comps.Length); foreach (Component 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) { int index = 0; for (int 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() { bool currentlyLocked = InspectorIsLocked(); bool wasJustLocked = currentlyLocked && !inspectorWasLocked; if (wasJustLocked) { PersistentData.SetDataForLockedInspector(InspectorWindow, inspectingObject); } bool 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() { float searchBarAndPadding = SearchBarHeight + SearchCompListSpace; if (Settings.TransOnlyKeepCopyPaste && OnlyHasTransform()) { return SearchBarHeight; } float[] 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. float 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; int noMultiEditIndex = editorListVisual.childCount; for (int i = 0; i < editorListVisual.childCount; i++) { if (editorListVisual[i].ClassListContains(InspectorNoMultiEditClassName)) { noMultiEditIndex = i; break; } } for (int i = noMultiEditIndex + 1; i < editorListVisual.childCount; i++) { noMultiEditVisualElements.Add(editorListVisual[i].name); } } private void CheckToShowContextMenu(List comps, List buttonRects) { bool mouseDown = Event.current.type is EventType.MouseDown; bool rightClicking = Event.current.button == 1; if (!mouseDown || !rightClicking) return; Event.current.Use(); // Eat event so right clicking doesn't toggle component GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("Copy Selection"), false, CopySelectedToClipboard); menu.AddItem(new GUIContent("Paste Clipboard"), false, PasteFromClipboard); Component compUnderCursor = GetComponentUnderCursor(comps, buttonRects); if (compUnderCursor) { menu.AddSeparator(""); string compName = compUnderCursor.GetType().Name; // Copy component menu.AddItem(new GUIContent($"Copy { compName }"), false, () => { PersistentData.Clipboard.CopyComponents(new() { compUnderCursor }); }); // Open component as script if (compUnderCursor is MonoBehaviour) { menu.AddItem(new GUIContent($"Edit { compName } Script"), false, () => { MonoScript 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 (int 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 (GameObject gameObject in Selection.gameObjects) { if (gameObject.TryGetComponent(compType, out Component 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 (GameObject gameObject in Selection.gameObjects) { gameObject.PasteComponents(PersistentData.Clipboard.Copies); } } private void CheckForShortcutOperations(List comps, List buttonRects) { if (activeShortcutToPerform == ShortcutOperation.ToggleComponent) { Component 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; } int 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; } } } } #endif