Skip to content

Instantly share code, notes, and snippets.

@TanaTanoi
Last active August 24, 2021 21:21
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save TanaTanoi/ca37a35628b5f8dcb47e67def3d7ee6b to your computer and use it in GitHub Desktop.
Save TanaTanoi/ca37a35628b5f8dcb47e67def3d7ee6b to your computer and use it in GitHub Desktop.
The DPC Editor tools framework, for making tool dev easy in Unity!
using UnityEditor;
using UnityEngine;
namespace DPCEditorTools
{
/// <summary>
/// An IEditorTool that manages the visibility of other editor tools.
/// </summary>
public class ActiveEditorToolsToggler : IEditorTool
{
string IEditorTool.Header => "Enabled Tools";
public void DrawTool(EditorToolsWindow callingEditorTool)
{
foreach (ToolState toolState in callingEditorTool.ToolStates)
{
// Skip this tool, can't disable it else we'd be in trouble.
if (toolState.tool == this)
{
continue;
}
DrawEditorCheckForTool(toolState, toolState.GetFormattedHeaderName());
}
if (GUILayout.Button("Show All"))
{
foreach (ToolState tool in callingEditorTool.ToolStates)
{
tool.IsEnabled = true;
}
}
if (GUILayout.Button("Hide All"))
{
foreach (ToolState tool in callingEditorTool.ToolStates)
{
tool.IsEnabled = false;
}
}
}
private void DrawEditorCheckForTool(ToolState toolState, string label)
{
GUIStyle style = new GUIStyle(EditorStyles.label);
if (toolState.IsFavourited)
{
style.normal.textColor = Color.yellow;
style.onNormal.textColor = Color.yellow;
}
toolState.IsEnabled = EditorGUILayout.ToggleLeft(label, toolState.IsEnabled, labelStyle: style);
}
public void OnCreated()
{
}
public bool OnlyShowWhenAGameIsAvailable()
{
return false;
}
}
}
using System;
using UnityEditor;
namespace DPCEditorTools
{
public class DPCIndividualEditorToolWindow : EditorWindow
{
private IEditorTool _windowedTool;
private IEditorTool WindowedTool
{
get
{
if (_windowedTool == null)
{
// In the event this window is created by other means (e.g. re-opening unity)
// Then we need to pull the window from the main ToolsWindow by name.
_windowedTool = ToolsWindow.GetToolByTypeString(titleContent.text).tool;
}
return _windowedTool;
}
}
private static EditorToolsWindow _toolsWindow;
private static EditorToolsWindow ToolsWindow
{
get
{
if (_toolsWindow == null)
{
_toolsWindow = GetWindow<EditorToolsWindow>();
}
return _toolsWindow;
}
}
public static void CreateWindow(Type type)
{
CreateWindow<DPCIndividualEditorToolWindow>(type.ToString());
}
private void OnGUI()
{
if (ToolsWindow != null && WindowedTool != null)
{
WindowedTool.DrawTool(ToolsWindow);
}
else
{
EditorGUILayout.LabelField("Please open the DPC Editor Window somewhere else too.");
}
}
private void OnDestroy()
{
if (ToolsWindow != null && WindowedTool != null)
{
// On Destroy, enable it back on the Editor Tools Window.
ToolsWindow.GetToolByTypeString(WindowedTool.GetType().ToString()).IsEnabled = true;
}
}
}
}
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.Profiling;
namespace DPCEditorTools
{
/// <summary>
/// EditorToolsWindow hosts all IEditorTools and is where you add new IEditorTool implementations.
/// It also manages which tools are expanded, saving state to EditorPrefs to ensure consistentcy.
/// </summary>
public class EditorToolsWindow : EditorWindow
{
[MenuItem("Window/DPC Editor Tools")]
public static void OpenDPCEditorToolsWindow()
{
GetWindow<EditorToolsWindow>(false, "DPC Editor Tools", true);
}
// Add your tools here! The order is the order in which they appear in the window.
private readonly List<Type> _toolTypes = new List<Type>
{
typeof(ShortcutsEditorTool),
};
private readonly Dictionary<Type, ToolState> _toolStates = new Dictionary<Type, ToolState>();
public IEnumerable<ToolState> ToolStates => _toolStates.Values;
private Vector2 _globalScrollView = Vector2.zero;
private static GUIStyle _defaultHeaderStyle;
private static GUIStyle _favouriteHeaderStyle;
// We use an Initialise method here instead of a unity invoked Awake because the call to `new GUIStyle`
// was throwing a NullReferenceException when the method was Awake.
// I'm not exactly sure what was causing it, but here is a link to someone else experiencing the issue:
// https://github.com/SubjectNerd-Unity/ReorderableInspector/issues/14
private void Initialise()
{
_defaultHeaderStyle = new GUIStyle(EditorStyles.foldoutHeader)
{
fontSize = 14,
fixedHeight = EditorStyles.foldoutHeader.fixedHeight + 5
};
_favouriteHeaderStyle = new GUIStyle(_defaultHeaderStyle)
{
onNormal = { textColor = Color.yellow },
normal = { textColor = Color.yellow },
onHover = { textColor = Color.yellow },
hover = { textColor = Color.yellow }
};
InitializeTool(typeof(ActiveEditorToolsToggler));
foreach (Type toolType in _toolTypes)
{
InitializeTool(toolType);
}
}
private void OnGUI()
{
Profiler.BeginSample("DPCEditorTool.OnGUI");
if (_toolStates.Count == 0)
{
Initialise();
}
_globalScrollView = EditorGUILayout.BeginScrollView(_globalScrollView);
EditorGUILayout.BeginVertical();
DrawTool(typeof(ActiveEditorToolsToggler), EditorStyles.foldoutHeader);
foreach (Type toolType in _toolTypes)
{
DrawTool(toolType);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
Profiler.EndSample();
}
private void InitializeTool(Type toolType)
{
if (!_toolStates.ContainsKey(toolType))
{
IEditorTool newInstance = (IEditorTool)Activator.CreateInstance(toolType);
newInstance.OnCreated();
_toolStates.Add(toolType, new ToolState(newInstance));
}
}
private Action<Rect> GetContextMenuActionForTool(Type toolType)
{
// Don't need one for the ActiveEditorToolsToggler.
if (toolType == typeof(ActiveEditorToolsToggler))
{
return null;
}
return (Rect rect) =>
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Open as new window"), false, () =>
{
CreateIndividualWindowForTool(toolType);
// Hide it so it can appear in a window instead
_toolStates[toolType].IsEnabled = false;
});
menu.AddItem(new GUIContent("Create new window of tool"), false, () => CreateIndividualWindowForTool(toolType));
menu.AddItem(new GUIContent("Hide tool"), false, () => _toolStates[toolType].IsEnabled = false);
menu.AddItem(new GUIContent("Toggle favourite"), false, () => _toolStates[toolType].ToggleIsFavourited());
menu.DropDown(rect);
};
}
public ToolState GetToolByTypeString(string toolTypeString)
{
if (_toolStates.Count == 0)
{
Initialise();
}
foreach (Type toolType in _toolStates.Keys)
{
if (toolType.ToString() == toolTypeString)
{
return _toolStates[toolType];
}
}
return null;
}
private void CreateIndividualWindowForTool(Type toolType)
{
DPCIndividualEditorToolWindow.CreateWindow(toolType);
}
private void DrawTool(Type toolType, GUIStyle styleOverride = null)
{
Profiler.BeginSample($"DPCEditorTool.DrawTool({toolType.Name})");
if (!_toolStates.TryGetValue(toolType, out ToolState activeToolState))
{
Debug.LogError($"There is no tool present for {toolType}!");
return;
}
GUIStyle style = styleOverride ?? (activeToolState.IsFavourited ? _favouriteHeaderStyle : _defaultHeaderStyle);
// Ensure the one that can hide everything (ActiveEditorToolsToggler) can't be hidden.
if (toolType == typeof(ActiveEditorToolsToggler) || activeToolState.IsEnabled)
{
string title = activeToolState.GetFormattedHeaderName();
activeToolState.IsExpanded = EditorGUILayout.BeginFoldoutHeaderGroup(activeToolState.IsExpanded, title, menuAction: GetContextMenuActionForTool(toolType), style: style);
if (activeToolState.IsExpanded)
{
using (new EditorGUI.IndentLevelScope())
{
activeToolState.tool.DrawTool(this);
}
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
Profiler.EndSample();
}
}
}
namespace DPCEditorTools
{
/// <summary>
/// An IEditorTool is used inside the EditorToolsWindow to draw a single, minimisable window.
/// </summary>
public interface IEditorTool
{
/// <summary>
/// What is this editor tool called?
/// </summary>
string Header { get; }
/// <summary>
/// If there's any on-creation logic you need before the tool is displayed, it can be done in this method.
/// This is called within the OnGUI call by EditorToolsWindow, but before DrawTool is called.
/// </summary>
void OnCreated();
/// <summary>
/// This is called as part of the immediate-mode editor OnGUI call by EditorToolsWindow.
/// This is only run if the tool is enabled.
/// </summary>
/// <param name="callingEditorTool"></param>
void DrawTool(EditorToolsWindow callingEditorTool);
}
}
using UnityEngine;
using UnityEditor;
namespace DPCEditorTools
{
public class ShortcutsEditorTool : IEditorTool
{
string IEditorTool.Header => "Shortcuts";
public void OnCreated() { }
public void DrawTool(EditorToolsWindow callingEditorTool)
{
using (new EditorGUI.DisabledScope(!Application.isPlaying))
{
Time.timeScale = EditorGUILayout.Slider("Unity Timescale", Time.timeScale, 0, 2);
}
}
}
}
using System;
using UnityEditor;
namespace DPCEditorTools
{
/// <summary>
/// This class stores other state about a tool, such as if the dropdown is expanded, if the header is favourited
/// or if the tool is visible in the main dropdown at all. It also hides all of the editor prefs behind properties,
/// which are used to keep the windows consistent between code reloads.
/// </summary>
public class ToolState
{
public ToolState(IEditorTool tool)
{
this.tool = tool;
}
private const string DpcEditorPrefsPrefix = "DPCEditorTool_";
private const string EditorPrefsHeaderExpandedPrefix = DpcEditorPrefsPrefix + "HeaderExpanded_";
private const string EditorPrefsToolEnabledPrefix = DpcEditorPrefsPrefix + "ToolEnabled_";
private const string EditorPrefsToolFavouritedPrefix = DpcEditorPrefsPrefix + "ToolFavourited_";
public IEditorTool tool;
public string GetFormattedHeaderName()
{
string headerName = tool.Header;
if (IsFavourited)
{
headerName += "*";
}
return headerName;
}
// Is this window expanded inside the EditorToolsWindow?
private bool? _isExpanded;
public bool IsExpanded
{
get
{
if (!_isExpanded.HasValue)
{
string key = GetEditorPrefsKeyForToolExpandedState(tool.GetType());
_isExpanded = EditorPrefs.GetBool(key, defaultValue: false);
}
return _isExpanded.Value;
}
set
{
if (!_isExpanded.HasValue || value != _isExpanded.Value)
{
_isExpanded = value;
string key = GetEditorPrefsKeyForToolExpandedState(tool.GetType());
EditorPrefs.SetBool(key, _isExpanded.Value);
}
}
}
// Is this tool window actually enabled to be displayed at all (even as a header)?
private bool? _isEnabled;
public bool IsEnabled
{
get
{
if (!_isEnabled.HasValue)
{
string key = GetEditorPrefsDisabledIdForWindow(tool.GetType());
_isEnabled = EditorPrefs.GetBool(key, defaultValue: true);
}
return _isEnabled.Value;
}
set
{
if (!_isEnabled.HasValue || _isEnabled.Value != value)
{
_isEnabled = value;
string key = GetEditorPrefsDisabledIdForWindow(tool.GetType());
EditorPrefs.SetBool(key, _isEnabled.Value);
}
}
}
// Is this tool window favourited by the user?
private bool? _isFavourited;
public bool IsFavourited
{
get
{
if (!_isFavourited.HasValue)
{
string key = GetEditorPrefsFavouriteIdForWindow(tool.GetType());
_isFavourited = EditorPrefs.GetBool(key, defaultValue: false);
}
return _isFavourited.Value;
}
set
{
if (!_isFavourited.HasValue || _isFavourited.Value != value)
{
_isFavourited = value;
string key = GetEditorPrefsFavouriteIdForWindow(tool.GetType());
EditorPrefs.SetBool(key, _isFavourited.Value);
}
}
}
public void ToggleIsFavourited()
{
IsFavourited = !IsFavourited;
}
private string GetEditorPrefsKeyForToolExpandedState(Type toolType)
{
return EditorPrefsHeaderExpandedPrefix + toolType.ToString();
}
public static string GetEditorPrefsFavouriteIdForWindow(Type toolType)
{
return EditorPrefsToolFavouritedPrefix + toolType.ToString();
}
public static string GetEditorPrefsDisabledIdForWindow(Type toolType)
{
return EditorPrefsToolEnabledPrefix + toolType.ToString();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment