Skip to content

Instantly share code, notes, and snippets.

@JLChnToZ
Last active January 4, 2022 07:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JLChnToZ/68b2a01b64fd6557f135e0ae3f7fa1e2 to your computer and use it in GitHub Desktop.
Save JLChnToZ/68b2a01b64fd6557f135e0ae3f7fa1e2 to your computer and use it in GitHub Desktop.
A tool for temporary holds selections in Unity
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityObject = UnityEngine.Object;
public class SelectionExtension: EditorWindow {
[MenuItem("Window/Selection Ex")]
public static void Open() => GetWindow<SelectionExtension>();
readonly Dictionary<UnityObject[], (string label, bool expanded)> savedCache = new Dictionary<UnityObject[], (string, bool)>(new SelectionEqualityComparer());
readonly HashSet<UnityObject> selectionLookup = new HashSet<UnityObject>();
readonly HashSet<UnityObject> selectionRecords = new HashSet<UnityObject>();
UnityObject[] selection;
string text = "";
Vector2 scrollPos;
bool isActiveSelectionExpanded;
bool isRecordSelectionExpanded;
bool recordMode;
void OnEnable() {
titleContent = new GUIContent("Selection Ex.");
UpdateSelection();
}
void OnFocus() => OnSelectionChange();
void OnGUI() {
var e = Event.current;
var eType = e.type;
if (eType == EventType.DragExited) DragAndDrop.PrepareStartDrag();
if (selection == null) UpdateSelection();
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
bool isEmptySelection = selection == null || selection.Length == 0;
bool disableAdd = isEmptySelection || savedCache.ContainsKey(selection);
EditorGUI.BeginDisabledGroup(disableAdd);
if (disableAdd) text = "";
text = EditorGUILayout.TextField(text, EditorStyles.toolbarTextField);
if (GUILayout.Button(EditorGUIUtility.IconContent("d_Toolbar Plus", "Save"), EditorStyles.toolbarButton)) {
text = ObjectNames.GetUniqueName(
(from entry in savedCache.Values select entry.label).ToArray(),
string.IsNullOrWhiteSpace(text) ? FormatItemNames(selection, "Selection") : text.Trim()
);
savedCache.Add(selection, (text, false));
text = "";
}
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(savedCache.Count == 0);
if (GUILayout.Button("Forget All", EditorStyles.toolbarButton)) savedCache.Clear();
EditorGUI.EndDisabledGroup();
EditorGUI.BeginChangeCheck();
recordMode = GUILayout.Toggle(recordMode, EditorGUIUtility.IconContent("Animation.Record", "Record Selection"), EditorStyles.toolbarButton);
if (EditorGUI.EndChangeCheck() && !recordMode) selectionRecords.Clear();
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
bool updateSelection = false;
if (!isEmptySelection) {
EditorGUILayout.BeginHorizontal();
isActiveSelectionExpanded = GUILayout.Toggle(isActiveSelectionExpanded, GUIContent.none, EditorStyles.foldout);
if (!DraggableToggle(new GUIContent("Active Selection", EditorGUIUtility.IconContent("Clipboard").image), true, selection)) {
selectionLookup.Clear();
updateSelection = true;
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
if (isActiveSelectionExpanded && DrawSelection(selection)) updateSelection = true;
}
if (selectionRecords.Count > 0) {
EditorGUILayout.BeginHorizontal();
isRecordSelectionExpanded = GUILayout.Toggle(isRecordSelectionExpanded, GUIContent.none, EditorStyles.foldout);
EditorGUILayout.LabelField(new GUIContent("Selection Records", EditorGUIUtility.IconContent("Clipboard").image));
GUILayout.FlexibleSpace();
if (GUILayout.Button(EditorGUIUtility.IconContent("d_TreeEditor.Trash", "Forget"), EditorStyles.label))
selectionRecords.Clear();
EditorGUILayout.EndHorizontal();
if (isRecordSelectionExpanded && DrawSelection(selectionRecords)) updateSelection = true;
}
UnityObject[] updateGroup = null;
(string label, bool expanded) updateGroupState = default;
bool isRemoveGroup = false;
foreach (var kv in savedCache) {
EditorGUILayout.BeginHorizontal();
bool selected = selectionLookup.Count > 0 && selectionLookup.Overlaps(kv.Key);
bool expanded = GUILayout.Toggle(kv.Value.expanded, GUIContent.none, EditorStyles.foldout);
EditorGUI.BeginChangeCheck();
EditorGUI.showMixedValue = selected && !selectionLookup.IsSupersetOf(kv.Key);
selected = DraggableToggle(new GUIContent(kv.Value.label, EditorGUIUtility.IconContent("Clipboard").image), selected, kv.Key);
EditorGUI.showMixedValue = false;
bool selectedChanged = EditorGUI.EndChangeCheck();
GUILayout.FlexibleSpace();
if (GUILayout.Button(EditorGUIUtility.IconContent("d_TreeEditor.Trash", "Forget"), EditorStyles.label)) isRemoveGroup = true;
EditorGUILayout.EndHorizontal();
if (selectedChanged) {
if (selected) {
selectionLookup.Clear();
selectionLookup.UnionWith(kv.Key);
} else
selectionLookup.ExceptWith(kv.Key);
updateSelection = true;
} else if ((isRemoveGroup || expanded != kv.Value.expanded) && updateGroup == null) {
updateGroup = kv.Key;
updateGroupState = kv.Value;
updateGroupState.expanded = expanded;
}
if (expanded && DrawSelection(kv.Key)) updateSelection = true;
}
EditorGUILayout.EndScrollView();
if (DragAndDrop.objectReferences.Length > 0)
switch (eType) {
case EventType.DragUpdated:
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
break;
case EventType.DragPerform:
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
DragAndDrop.AcceptDrag();
selectionLookup.UnionWith(DragAndDrop.objectReferences);
updateSelection = true;
break;
}
if (updateSelection) {
if (selection == null || selection.Length != selectionLookup.Count)
selection = new UnityObject[selectionLookup.Count];
selectionLookup.CopyTo(selection);
Selection.objects = selection;
OnSelectionChange();
}
if (updateGroup != null) {
if (isRemoveGroup) savedCache.Remove(updateGroup);
else savedCache[updateGroup] = updateGroupState;
}
}
bool DrawSelection(IEnumerable<UnityObject> selection) {
EditorGUI.indentLevel += 2;
bool updateSelection = false;
foreach (var entry in selection) {
if (entry == null) continue;
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginChangeCheck();
bool isChecked = DraggableToggle(EditorGUIUtility.ObjectContent(entry, entry.GetType()), selectionLookup.Contains(entry), entry);
if (EditorGUI.EndChangeCheck()) {
updateSelection = true;
if (isChecked) selectionLookup.Add(entry);
else selectionLookup.Remove(entry);
}
GUILayout.FlexibleSpace();
if (GUILayout.Button(EditorGUIUtility.IconContent("d_UnityEditor.FindDependencies", $"Ping {entry.name}"), EditorStyles.label))
EditorGUIUtility.PingObject(entry);
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel -= 2;
return updateSelection;
}
bool DraggableToggle(GUIContent label, bool value, params UnityObject[] dragTargets) {
var e = Event.current;
Rect rect = EditorGUILayout.GetControlRect(true);
if (dragTargets != null && dragTargets.Length > 0 && rect.Contains(e.mousePosition))
switch (e.type) {
case EventType.MouseUp:
DragAndDrop.PrepareStartDrag();
break;
case EventType.MouseDown:
DragAndDrop.PrepareStartDrag();
DragAndDrop.objectReferences = dragTargets;
break;
case EventType.MouseDrag:
DragAndDrop.objectReferences = dragTargets;
DragAndDrop.StartDrag(FormatItemNames(dragTargets));
e.Use();
break;
}
return EditorGUI.ToggleLeft(rect, label, value);
}
string FormatItemNames(UnityObject[] items, string defaultName = null) {
var defaultItem = items.FirstOrDefault();
string name = defaultItem != null ? defaultItem.name : defaultName ?? string.Empty;
return items.Length > 1 ?
string.IsNullOrWhiteSpace(name) ?
$"{items.Length - 1} Items" :
$"{name} (+{items.Length - 1} Items)" :
name;
}
void OnSelectionChange() {
UpdateSelection();
Repaint();
}
void UpdateSelection() {
selection = Selection.objects;
selectionLookup.Clear();
selectionLookup.UnionWith(selection);
if (recordMode) selectionRecords.UnionWith(selectionLookup);
}
}
class SelectionEqualityComparer: IEqualityComparer<UnityObject[]> {
readonly HashSet<UnityObject> cache = new HashSet<UnityObject>();
public bool Equals(UnityObject[] lhs, UnityObject[] rhs) {
if (lhs == null) return rhs == null;
if (rhs == null) return false;
try {
foreach (var entry in lhs) if (entry != null) cache.Add(entry);
foreach (var entry in rhs) if (entry != null && !cache.Contains(entry)) return false;
return true;
} finally {
cache.Clear();
}
}
public int GetHashCode(UnityObject[] obj) {
if (obj == null || obj.Length == 0) return 0;
int hashCode = 0;
foreach (var entry in obj) unchecked {
if (entry != null) hashCode ^= entry.GetInstanceID();
}
return hashCode;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment