Skip to content

Instantly share code, notes, and snippets.

@edwardrowe
Created May 12, 2020 13:08
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 edwardrowe/632cf972c5d024756fad5d7b79ce134e to your computer and use it in GitHub Desktop.
Save edwardrowe/632cf972c5d024756fad5d7b79ce134e to your computer and use it in GitHub Desktop.
Selection History management window for Unity
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Selection history window watches the selection and adds Forward and Back buttons to navigate previous selections.
/// </summary>
public class SelectionHistoryWindow : EditorWindow, IHasCustomMenu
{
private const string BackMenuPath = "Edit/Select Previous #F1";
private const string ForwardMenuPath = "Edit/Select Next #F2";
private const string WindowMenuPath = "Window/Selection History Window";
private const int HistorySize = 10;
private bool ignoreSelectionChange;
private int[] currentSelection;
private Stack<int[]> selectionHistory;
private Stack<int[]> poppedSelections;
/// <summary>
/// Implements IHasCutomMenu callback to add items into the right click menu and context menu on the window.
/// </summary>
/// <param name="menu">Menu to add into.</param>
public virtual void AddItemsToMenu(GenericMenu menu)
{
menu.AddItem(new GUIContent("Clear History"), false, new GenericMenu.MenuFunction(this.ClearHistory));
}
[MenuItem(WindowMenuPath)]
private static SelectionHistoryWindow OpenSelectionHistoryWindow()
{
var window = EditorWindow.GetWindow<SelectionHistoryWindow>(false, "History");
return window;
}
[MenuItem(BackMenuPath, false, 50)]
private static void BackMenuItem()
{
var window = OpenSelectionHistoryWindow();
window.MoveSelectionBack();
}
[MenuItem(ForwardMenuPath, false, 51)]
private static void ForwardMenuItem()
{
var window = OpenSelectionHistoryWindow();
window.MoveSelectionForward();
}
private void OnEnable()
{
this.minSize = new Vector2(minSize.x, 25.0f);
// Note this clears selection every time the game recompiles.
this.poppedSelections = new Stack<int[]>(HistorySize);
this.selectionHistory = new Stack<int[]>(HistorySize);
}
private void ClearHistory()
{
this.selectionHistory.Clear();
this.poppedSelections.Clear();
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(!this.IsBackEnabled());
var leftArrowCode = "\u2190";
if (GUILayout.Button(leftArrowCode, GUILayout.MinWidth(35)))
{
this.MoveSelectionBack();
}
EditorGUI.EndDisabledGroup();
this.GUIDrawLabelForStack(this.selectionHistory, true);
GUILayout.FlexibleSpace();
this.GUIDrawLabelForStack(this.poppedSelections, false);
EditorGUI.BeginDisabledGroup(!this.IsForwardEnabled());
var rightArrowCode = "\u2192";
if (GUILayout.Button(rightArrowCode, GUILayout.MinWidth(35)))
{
this.MoveSelectionForward();
}
EditorGUI.EndDisabledGroup();
EditorGUILayout.EndHorizontal();
}
private bool IsBackEnabled()
{
if (this.selectionHistory == null)
{
return false;
}
return this.selectionHistory.Count > 0;
}
private void MoveSelectionBack()
{
if (this.IsBackEnabled())
{
this.PushCurrentSelectionAndPopNext(this.poppedSelections, this.selectionHistory);
}
}
private bool IsForwardEnabled()
{
if (this.poppedSelections == null)
{
return false;
}
return this.poppedSelections.Count > 0;
}
private void MoveSelectionForward()
{
if (this.IsForwardEnabled())
{
this.PushCurrentSelectionAndPopNext(this.selectionHistory, this.poppedSelections);
}
}
private void PushCurrentSelectionAndPopNext(Stack<int[]> pushToStack, Stack<int[]> popFromStack)
{
// Set flag so that this selection change is ignored during the selection callback
this.ignoreSelectionChange = true;
// Add current selection to a stack that maintains history
pushToStack.Push(this.currentSelection);
this.currentSelection = popFromStack.Pop();
// Selected the popped value
Selection.instanceIDs = this.currentSelection;
}
private void GUIDrawLabelForStack(Stack<int[]> stack, bool alignLeft)
{
var itemName = string.Empty;
if (stack.Count > 0)
{
var nextItems = stack.Peek();
itemName = this.GetStringForInstanceIDs(nextItems);
}
GUIStyle textStyle = new GUIStyle(EditorStyles.label);
textStyle.alignment = alignLeft ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight;
EditorGUILayout.LabelField(itemName, textStyle, GUILayout.MinWidth(40), GUILayout.ExpandWidth(true));
}
private void OnSelectionChange()
{
if (this.ignoreSelectionChange)
{
this.ignoreSelectionChange = false;
return;
}
// Push old selection onto the history, unless there is no selection (first selection ever)
if (this.currentSelection != null)
{
// This is the same group. do not affect history
if (!this.IsSelectionAddative())
{
// Selection represents a new selection.
this.selectionHistory.Push(this.currentSelection);
}
}
this.currentSelection = Selection.instanceIDs;
// Clear forward history whenever any selection change is made
this.poppedSelections.Clear();
this.Repaint();
}
private bool IsSelectionAddative()
{
var selectedIDs = Selection.instanceIDs;
// Can't be addative if only one thing is selected
if (selectedIDs.Length > 1)
{
// If current (previous) selection contains one of the newly selected IDs, this must be a shift+select or drag select
for (int i = 0; i < this.currentSelection.Length; ++i)
{
var currentSelectionID = this.currentSelection[i];
for (int idIndex = 0; idIndex < selectedIDs.Length; ++idIndex)
{
var id = selectedIDs[idIndex];
if (id == currentSelectionID)
{
return true;
}
}
}
}
return false;
}
private string GetStringForInstanceIDs(int[] ids)
{
string name = string.Empty;
if (ids.Length > 0)
{
var instanceID = ids[0];
UnityEngine.Object firstObject = EditorUtility.InstanceIDToObject(instanceID);
if (firstObject != null)
{
name = firstObject.name;
}
else
{
name = "[Destroyed]";
}
if (ids.Length > 1)
{
name = string.Concat("[", ids.Length, "]", name);
}
}
else
{
name = "[None]";
}
return name;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment