Files
Continentis/Assets/OtherPlugins/Wingman/WingmanContainer.cs
SoulliesOfficial d09b58fd80 架构大更
2026-03-20 11:56:50 -04:00

1430 lines
52 KiB
C#

#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<int, Component> 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<string> noMultiEditVisualElements = new();
private bool performSearchFlag;
private IMGUIContainer pinnedDividerContainer;
private IMGUIContainer pinnedHeaderContainer;
private readonly List<int> prevValidCompIds = new();
private int rangeModifierPivot;
private List<ComponentSearchResults> searchResults = new();
private IMGUIContainer searchResultsGuiContainer;
private List<int> selectedCompIds;
private double timeOfLastSearchUpdate;
private readonly List<int> 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<Rect> placementRects, List<Component> 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<Rect> GetButtonPlacements(Rect scrollViewRect, List<Component> comps, float[] buttonWidths)
{
var placements = new List<Rect>();
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<Component> GetComponentsFromSelection()
{
if (!InspectingObjectIsValid()) return null;
var allComps = GetAllVisibleComponents();
if (AllIsSelected()) return allComps;
var selComps = new List<Component>(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<ComponentSearchResults>();
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<int> list0, List<int> 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<Component>().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<Component> 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<SerializedProperty> GetComponentFields(SerializedObject serializedComponent)
{
var iter = serializedComponent.GetIterator();
if (iter == null || !iter.NextVisible(true)) return null;
var fields = new List<SerializedProperty>();
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<Component> 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<Component> list, Type componentType)
{
foreach (var component in list)
if (component.GetType() == componentType)
return true;
return false;
}
private List<Component> GetAllVisibleComponents(GameObject gameObject)
{
var comps = gameObject.GetComponents<Component>();
var res = new List<Component>(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<Component> comps, List<Rect> 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<Component> { 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<Component> comps, List<Rect> 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<Component> comps, List<Rect> 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<SerializedProperty> Fields = new();
public SerializedObject SerializedComponent;
}
}
}
#endif