浏览代码

"Replace" context-menu with a searchable popup (#223)

* "Replace" context-menu with a popup window

* [Bot] Automated dotnet-format update

* Selection interactive preview

* Whoops, wrong merge resolve

* [Bot] Automated dotnet-format update

* Show popup at screen mouse position

* Mini-previews on renderable objects + minor GUI improvements

* [Bot] Automated dotnet-format update

* Editors cache fix, remove dummy editor

* Destroy only cached editor

* Rename "ReplacePrefabTreeView" to "PrefabSelectionTreeView"

* catch ClearPreviewCache error

* fix null texture warning

* Fix popup fit on smaller screens

* Fix black frame when closing popup during repaint

* Expand folder on single-click

* Remove editor Destroy, as error is still showing inside try catch

* Revert delayed close, as it made popup not-closable in some cases
/main
GitHub 4 年前
当前提交
851182b5
共有 9 个文件被更改,包括 662 次插入3 次删除
  1. 6
      UOP1_Project/Assets/Scripts/Editor/ReplaceTool.cs
  2. 113
      UOP1_Project/Assets/Scripts/Editor/GameObjectPreview.cs
  3. 3
      UOP1_Project/Assets/Scripts/Editor/GameObjectPreview.cs.meta
  4. 267
      UOP1_Project/Assets/Scripts/Editor/PrefabSelectionTreeView.cs
  5. 11
      UOP1_Project/Assets/Scripts/Editor/PrefabSelectionTreeView.cs.meta
  6. 69
      UOP1_Project/Assets/Scripts/Editor/ReplaceContextMenu.cs
  7. 3
      UOP1_Project/Assets/Scripts/Editor/ReplaceContextMenu.cs.meta
  8. 190
      UOP1_Project/Assets/Scripts/Editor/ReplacePrefabSearchPopup.cs
  9. 3
      UOP1_Project/Assets/Scripts/Editor/ReplacePrefabSearchPopup.cs.meta

6
UOP1_Project/Assets/Scripts/Editor/ReplaceTool.cs


/// </summary>
/// <param name="objectToReplace">Game Objects to replace.</param>
/// <param name="replaceObject">Prefab that will be instantiated in place of the objects to replace.</param>
private void ReplaceSelectedObjects(GameObject[] objectToReplace, GameObject replaceObject)
internal static void ReplaceSelectedObjects(GameObject[] objectToReplace, GameObject replaceObject)
Debug.Log("[Replace Tool] Replace process");
//Debug.Log("[Replace Tool] Replace process");
for (int i = 0; i < objectToReplace.Length; i++)
{
var go = objectToReplace[i];

}
Undo.DestroyObjectImmediate(go);
}
Debug.LogFormat("[Replace Tool] {0} objects replaced on scene with {1}", objectToReplace.Length, replaceObject.name);
//Debug.LogFormat("[Replace Tool] {0} objects replaced on scene with {1}", objectToReplace.Length, replaceObject.name);
}
}

113
UOP1_Project/Assets/Scripts/Editor/GameObjectPreview.cs


using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UOP1.EditorTools
{
internal class GameObjectPreview
{
private static Type gameObjectInspectorType;
private static MethodInfo getPreviewDataMethod;
private static FieldInfo renderUtilityField;
private Rect renderRect;
private Color light0Color;
private Color light1Color;
private PreviewRenderUtility renderUtility;
private Editor cachedEditor;
public RenderTexture outputTexture;
[InitializeOnLoadMethod]
private static void OnInitialize()
{
gameObjectInspectorType = typeof(Editor).Assembly.GetType("UnityEditor.GameObjectInspector");
var previewDataType = gameObjectInspectorType.GetNestedType("PreviewData", BindingFlags.NonPublic);
getPreviewDataMethod = gameObjectInspectorType.GetMethod("GetPreviewData", BindingFlags.NonPublic | BindingFlags.Instance);
renderUtilityField = previewDataType.GetField("renderUtility", BindingFlags.Public | BindingFlags.Instance);
}
public void CreatePreviewForTarget(GameObject target)
{
if (!cachedEditor || cachedEditor.target != target)
{
renderUtility = null;
// There is a bug that breaks previews and Prefab mode after creating too many editors.
// Simply using CreateCachedEditor is fixing that problem.
Editor.CreateCachedEditor(target, gameObjectInspectorType, ref cachedEditor);
}
}
public void RenderInteractivePreview(Rect rect)
{
if (!cachedEditor)
return;
if (renderUtility == null || renderUtility.lights[0] == null)
{
var previewData = getPreviewDataMethod.Invoke(cachedEditor, null);
renderUtility = renderUtilityField.GetValue(previewData) as PreviewRenderUtility;
light0Color = renderUtility.lights[0].color;
light1Color = renderUtility.lights[1].color;
}
renderUtility.lights[0].color = light0Color * 1.6f;
renderUtility.lights[1].color = light1Color * 6f;
var backColor = renderUtility.camera.backgroundColor;
renderUtility.camera.backgroundColor = new Color(backColor.r, backColor.g, backColor.b, 0);
renderUtility.camera.clearFlags = CameraClearFlags.Depth;
var color = GUI.color;
// Hide default preview texture, since it has no alpha blending
GUI.color = new Color(1, 1, 1, 0);
cachedEditor.OnPreviewGUI(rect, null);
GUI.color = color;
outputTexture = renderUtility.camera.targetTexture;
}
public void DrawPreviewTexture(Rect rect)
{
GUI.DrawTexture(rect, outputTexture, ScaleMode.ScaleToFit, true, 0);
}
public static bool HasRenderableParts(GameObject go)
{
var renderers = go.GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
switch (renderer)
{
case MeshRenderer _:
var filter = renderer.gameObject.GetComponent<MeshFilter>();
if (filter && filter.sharedMesh)
return true;
break;
case SkinnedMeshRenderer skinnedMesh:
if (skinnedMesh.sharedMesh)
return true;
break;
case SpriteRenderer sprite:
if (sprite.sprite)
return true;
break;
case BillboardRenderer billboard:
if (billboard.billboard && billboard.sharedMaterial)
return true;
break;
}
}
return false;
}
}
}

3
UOP1_Project/Assets/Scripts/Editor/GameObjectPreview.cs.meta


fileFormatVersion: 2
guid: bb1d562e678248bbb3b897536cf62c50
timeCreated: 1606495882

267
UOP1_Project/Assets/Scripts/Editor/PrefabSelectionTreeView.cs


using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UOP1.EditorTools.Replacer
{
internal class PrefabSelectionTreeView : TreeView
{
private static Texture2D prefabOnIcon = EditorGUIUtility.IconContent("Prefab On Icon").image as Texture2D;
private static Texture2D prefabVariantOnIcon = EditorGUIUtility.IconContent("PrefabVariant On Icon").image as Texture2D;
private static Texture2D folderIcon = EditorGUIUtility.IconContent("Folder Icon").image as Texture2D;
private static Texture2D folderOnIcon = EditorGUIUtility.IconContent("Folder On Icon").image as Texture2D;
private static GUIStyle whiteLabel;
private static GUIStyle foldout;
public int RowsCount => rows.Count;
private Event evt => Event.current;
public Action<GameObject> onSelectEntry;
private List<TreeViewItem> rows = new List<TreeViewItem>();
private HashSet<string> paths = new HashSet<string>();
private Dictionary<int, RenderTexture> previewCache = new Dictionary<int, RenderTexture>();
private HashSet<int> renderableItems = new HashSet<int>();
private GameObjectPreview itemPreview = new GameObjectPreview();
private GUIContent itemContent = new GUIContent();
private int selectedId;
public PrefabSelectionTreeView(TreeViewState state) : base(state)
{
foldoutOverride = FoldoutOverride;
Reload();
}
private bool FoldoutOverride(Rect position, bool expandedState, GUIStyle style)
{
position.width = Screen.width;
position.height = 20;
position.y -= 2;
expandedState = GUI.Toggle(position, expandedState, GUIContent.none, style);
return expandedState;
}
public void Cleanup()
{
foreach (var texture in previewCache.Values)
Object.DestroyImmediate(texture);
}
public bool IsRenderable(int id)
{
return renderableItems.Contains(id);
}
private void CachePreview(int itemId)
{
var copy = new RenderTexture(itemPreview.outputTexture);
var previous = RenderTexture.active;
Graphics.Blit(itemPreview.outputTexture, copy);
RenderTexture.active = previous;
previewCache.Add(itemId, copy);
}
protected override bool CanMultiSelect(TreeViewItem item)
{
return false;
}
private bool IsPrefabAsset(int id, out GameObject prefab)
{
var obj = EditorUtility.InstanceIDToObject(id);
if (obj is GameObject go)
{
prefab = go;
return true;
}
prefab = null;
return false;
}
protected override void DoubleClickedItem(int id)
{
if (IsPrefabAsset(id, out var prefab))
onSelectEntry(prefab);
else
SetExpanded(id, !IsExpanded(id));
}
protected override void KeyEvent()
{
var key = evt.keyCode;
if (key == KeyCode.KeypadEnter || key == KeyCode.Return)
DoubleClickedItem(selectedId);
}
protected override void SelectionChanged(IList<int> selectedIds)
{
if (selectedIds.Count > 0)
selectedId = selectedIds[0];
}
protected override TreeViewItem BuildRoot()
{
var root = new TreeViewItem(0, -1);
rows.Clear();
paths.Clear();
foreach (var guid in AssetDatabase.FindAssets("t:Prefab"))
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var splits = path.Split('/');
var depth = splits.Length - 2;
if (splits[0] != "Assets")
break;
var asset = AssetDatabase.LoadAssetAtPath<GameObject>(path);
AddFoldersItems(splits);
AddPrefabItem(asset, depth);
}
SetupParentsAndChildrenFromDepths(root, rows);
return root;
}
protected override float GetCustomRowHeight(int row, TreeViewItem item)
{
// Hide folders during search
if (!IsPrefabAsset(item.id, out _) && hasSearch)
return 0;
return 20;
}
public override void OnGUI(Rect rect)
{
if (whiteLabel == null)
whiteLabel = new GUIStyle(EditorStyles.label) { normal = { textColor = EditorStyles.whiteLabel.normal.textColor } };
base.OnGUI(rect);
}
protected override void RowGUI(RowGUIArgs args)
{
var rect = args.rowRect;
var item = args.item;
var isRenderable = IsRenderable(item.id);
var isSelected = IsSelected(item.id);
var isFocused = HasFocus() && isSelected;
var isPrefab = IsPrefabAsset(item.id, out var prefab);
var isFolder = !isPrefab;
if (isFolder && hasSearch)
return;
if (isFolder)
{
if (rect.Contains(evt.mousePosition) && evt.type == EventType.MouseUp)
{
SetSelection(new List<int> { item.id });
SetFocus();
}
}
var labelStyle = isFocused ? whiteLabel : EditorStyles.label;
var contentIndent = GetContentIndent(item);
customFoldoutYOffset = 2;
itemContent.text = item.displayName;
rect.x += contentIndent;
rect.width -= contentIndent;
var iconRect = new Rect(rect) { width = 20 };
if (isPrefab)
{
var type = PrefabUtility.GetPrefabAssetType(prefab);
var onIcon = type == PrefabAssetType.Regular ? prefabOnIcon : prefabVariantOnIcon;
var labelRect = new Rect(rect);
if (isRenderable)
{
var previewRect = new Rect(rect) { width = 32, height = 32 };
if (!previewCache.TryGetValue(item.id, out var previewTexture))
{
itemPreview.CreatePreviewForTarget(prefab);
itemPreview.RenderInteractivePreview(previewRect);
if (itemPreview.outputTexture)
CachePreview(item.id);
}
if (!previewTexture)
Repaint();
else
GUI.DrawTexture(iconRect, previewTexture, ScaleMode.ScaleAndCrop);
labelRect.x += iconRect.width;
labelRect.width -= iconRect.width + 24;
GUI.Label(labelRect, args.label, labelStyle);
if (isSelected)
{
var prefabIconRect = new Rect(iconRect) { x = rect.xMax - 24 };
GUI.Label(prefabIconRect, isFocused ? onIcon : item.icon);
}
}
else
{
itemContent.image = isSelected ? onIcon : item.icon;
GUI.Label(rect, itemContent, labelStyle);
}
}
else
{
itemContent.image = isFocused ? folderOnIcon : folderIcon;
GUI.Label(rect, itemContent, labelStyle);
}
}
private void AddFoldersItems(string[] splits)
{
for (int i = 1; i < splits.Length - 1; i++)
{
var split = splits[i];
if (!paths.Contains(split))
{
rows.Add(new TreeViewItem(split.GetHashCode(), i - 1, " " + split) { icon = folderIcon });
paths.Add(split);
}
}
}
private void AddPrefabItem(GameObject asset, int depth)
{
var id = asset.GetInstanceID();
var content = new GUIContent(EditorGUIUtility.ObjectContent(asset, asset.GetType()));
if (GameObjectPreview.HasRenderableParts(asset))
renderableItems.Add(id);
rows.Add(new TreeViewItem(id, depth, content.text)
{
icon = content.image as Texture2D
});
}
}
}

11
UOP1_Project/Assets/Scripts/Editor/PrefabSelectionTreeView.cs.meta


fileFormatVersion: 2
guid: 241edd7da6766964687c5bf319a6163f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

69
UOP1_Project/Assets/Scripts/Editor/ReplaceContextMenu.cs


using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace UOP1.EditorTools.Replacer
{
internal class ReplaceContextMenu
{
private static Type hierarchyType;
private static EditorWindow focusedWindow;
private static IMGUIContainer hierarchyGUI;
private static Vector2 mousePosition;
private static bool hasExecuted;
[InitializeOnLoadMethod]
private static void OnInitialize()
{
hierarchyType = typeof(Editor).Assembly.GetType("UnityEditor.SceneHierarchyWindow");
EditorApplication.update += TrackFocusedHierarchy;
}
private static void TrackFocusedHierarchy()
{
if (focusedWindow != EditorWindow.focusedWindow)
{
focusedWindow = EditorWindow.focusedWindow;
if (focusedWindow?.GetType() == hierarchyType)
{
if (hierarchyGUI != null)
hierarchyGUI.onGUIHandler -= OnFocusedHierarchyGUI;
hierarchyGUI = focusedWindow.rootVisualElement.parent.Query<IMGUIContainer>();
hierarchyGUI.onGUIHandler += OnFocusedHierarchyGUI;
}
}
}
private static void OnFocusedHierarchyGUI()
{
// As Event.current is null during context-menu callback, we need to track mouse position on hierarchy GUI
mousePosition = GUIUtility.GUIToScreenPoint(Event.current.mousePosition);
}
[MenuItem("GameObject/Replace", true, priority = 0)]
private static bool ReplaceSelectionValidate()
{
return Selection.gameObjects.Length > 0;
}
[MenuItem("GameObject/Replace", priority = 0)]
private static void ReplaceSelection()
{
if (hasExecuted)
return;
var rect = new Rect(mousePosition, new Vector2(240, 360));
ReplacePrefabSearchPopup.Show(rect);
EditorApplication.delayCall += () => hasExecuted = false;
}
}
}

3
UOP1_Project/Assets/Scripts/Editor/ReplaceContextMenu.cs.meta


fileFormatVersion: 2
guid: a36e948ccc254a1cbffab62792cda09d
timeCreated: 1606469766

190
UOP1_Project/Assets/Scripts/Editor/ReplacePrefabSearchPopup.cs


using System;
using System.IO;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using static UnityEditor.EditorGUIUtility;
using static UnityEditor.EditorJsonUtility;
using static UnityEngine.Application;
namespace UOP1.EditorTools.Replacer
{
internal class ReplacePrefabSearchPopup : EditorWindow
{
private const float previewHeight = 128;
private class ViewState : ScriptableObject
{
public TreeViewState treeViewState = new TreeViewState();
}
private static ReplacePrefabSearchPopup window;
private static Styles styles;
private static Event evt => Event.current;
private static string assetPath => Path.Combine(dataPath.Remove(dataPath.Length - 7, 7), "Library", "ReplacePrefabTreeState.asset");
private bool hasSelection => tree.state.selectedIDs.Count > 0;
private int selectedId => tree.state.selectedIDs[0];
private GameObject instance => EditorUtility.InstanceIDToObject(selectedId) as GameObject;
private SearchField searchField;
private PrefabSelectionTreeView tree;
private ViewState viewState;
private Vector2 startPos;
private Vector2 startSize;
private Vector2 lastSize;
private GameObjectPreview selectionPreview = new GameObjectPreview();
public static void Show(Rect rect)
{
var windows = Resources.FindObjectsOfTypeAll<ReplacePrefabSearchPopup>();
window = windows.Length != 0 ? windows[0] : CreateInstance<ReplacePrefabSearchPopup>();
window.Init();
window.startPos = rect.position;
window.startSize = rect.size;
window.position = new Rect(rect.position, rect.size);
// Need to predict start window size to avoid trash frame
window.SetInitialSize();
// This type of window supports resizing, but is also persistent, so we need to close it manually
window.ShowPopup();
//onSelectEntry += _ => window.Close();
}
private void Init()
{
viewState = CreateInstance<ViewState>();
if (File.Exists(assetPath))
FromJsonOverwrite(File.ReadAllText(assetPath), viewState);
tree = new PrefabSelectionTreeView(viewState.treeViewState);
tree.onSelectEntry += OnSelectEntry;
AssetPreview.SetPreviewTextureCacheSize(tree.RowsCount);
searchField = new SearchField();
searchField.downOrUpArrowKeyPressed += tree.SetFocusAndEnsureSelectedItem;
searchField.SetFocus();
}
private void OnSelectEntry(GameObject prefab)
{
ReplaceTool.ReplaceSelectedObjects(Selection.gameObjects, prefab);
}
private void OnEnable()
{
Init();
}
private void OnDisable()
{
tree.Cleanup();
}
public new void Close()
{
SaveState();
base.Close();
}
private void SaveState()
{
File.WriteAllText(assetPath, ToJson(viewState));
}
private void OnGUI()
{
if (evt.type == EventType.KeyDown && evt.keyCode == KeyCode.Escape)
{
if (tree.hasSearch)
tree.searchString = "";
else
Close();
}
if (focusedWindow != this)
Close();
if (styles == null)
styles = new Styles();
DoToolbar();
DoTreeView();
DoSelectionPreview();
}
void DoToolbar()
{
tree.searchString = searchField.OnToolbarGUI(tree.searchString);
GUILayout.Label("Replace With...", styles.headerLabel);
}
void DoTreeView()
{
var rect = GUILayoutUtility.GetRect(0, 10000, 0, 10000);
rect.x += 2;
rect.width -= 4;
rect.y += 2;
rect.height -= 4;
tree.OnGUI(rect);
}
void DoSelectionPreview()
{
if (hasSelection && tree.IsRenderable(selectedId))
{
SetSize(startSize.x, startSize.y + previewHeight);
var previewRect = GUILayoutUtility.GetRect(position.width, previewHeight);
selectionPreview.CreatePreviewForTarget(instance);
selectionPreview.RenderInteractivePreview(previewRect);
selectionPreview.DrawPreviewTexture(previewRect);
}
else
{
SetSize(startSize.x, startSize.y);
}
}
private void SetInitialSize()
{
if (hasSelection && tree.IsRenderable(selectedId))
SetSize(startSize.x, startSize.y + previewHeight);
else
SetSize(startSize.x, startSize.y);
}
private void SetSize(float width, float height)
{
var newSize = new Vector2(width, height);
if (newSize != lastSize)
{
lastSize = newSize;
position = new Rect(position.x, position.y, width, height);
}
}
private class Styles
{
public GUIStyle headerLabel = new GUIStyle(EditorStyles.centeredGreyMiniLabel)
{
fontSize = 11,
fontStyle = FontStyle.Bold
};
}
}
}

3
UOP1_Project/Assets/Scripts/Editor/ReplacePrefabSearchPopup.cs.meta


fileFormatVersion: 2
guid: 566745c65b224919bb9886aba499be67
timeCreated: 1606459889
正在加载...
取消
保存