Skip to content

Instantly share code, notes, and snippets.

@kraj0t
Last active April 28, 2022 22:08
Show Gist options
  • Save kraj0t/cd79b7fbad917f6f1897b760ee98b78c to your computer and use it in GitHub Desktop.
Save kraj0t/cd79b7fbad917f6f1897b760ee98b78c to your computer and use it in GitHub Desktop.
DisplayKeyboardShortcuts - a Unity editor tool that shows the keys and shortcuts that the user is pressing
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEditor.ShortcutManagement;
using UnityEngine;
[InitializeOnLoad]
internal class DisplayKeyboardShortcuts
{
private const string SETTINGS_PATH = "_AURELIO/Display Shortcuts";
private const string KEY_ENABLED = "DisplayKeyboardShortcuts_enabled";
private const string KEY_ONLYSHOWKEYS = "DisplayKeyboardShortcuts_onlyShowKeys";
private const string KEY_SHOWFULLSHORTCUTPATHS = "DisplayKeyboardShortcuts_showFullShortcutPaths";
private const string KEY_IGNOREKEYPRESSESWITHOUTSHORTCUTS =
"DisplayKeyboardShortcuts_ignoreKeypressesWithoutShortcut";
private const int NOTIFICATION_MAX_CHARS_PER_LINE = 20;
private static readonly GUIContent _notificationGuiContent = new GUIContent();
private static readonly StringBuilder _sb = new StringBuilder();
private static bool _onlyShowKeys = false;
private static bool _showFullShortcutPaths = false;
private static bool _ignoreKeypressesWithoutShortcut = false;
private static KeyCombination _pressedKeyCombination;
// Cache the reflection stuff to skip as much of it as possible on every call.
private static MethodInfo _findShortcutsInternalMethodInfo;
private static PropertyInfo _shortcutControllerPropertyInfo;
private static IEnumerable<object> _listInternalShortcutEntries;
private static bool Enabled => EditorPrefs.GetBool(KEY_ENABLED, true);
static DisplayKeyboardShortcuts()
{
if (Enabled)
{
Enable();
}
}
[SettingsProvider]
private static SettingsProvider CreateMyCustomSettingsProvider()
{
var provider = new SettingsProvider(SETTINGS_PATH, SettingsScope.User)
{
guiHandler = (searchContext) =>
{
// Get current values.
var enabled = Enabled;
_onlyShowKeys = EditorPrefs.GetBool(KEY_ONLYSHOWKEYS, _onlyShowKeys);
_showFullShortcutPaths = EditorPrefs.GetBool(KEY_SHOWFULLSHORTCUTPATHS, _showFullShortcutPaths);
_ignoreKeypressesWithoutShortcut = EditorPrefs.GetBool(KEY_IGNOREKEYPRESSESWITHOUTSHORTCUTS,
_ignoreKeypressesWithoutShortcut);
// Ask the user for new values.
EditorGUI.BeginChangeCheck();
EditorGUIUtility.labelWidth += 120;
enabled = EditorGUILayout.Toggle("Enabled", enabled);
using var enabledScope = new EditorGUI.DisabledScope(!enabled);
bool newOnlyShowKeys = EditorGUILayout.Toggle("Only show keys", _onlyShowKeys);
using var onlyShowKeysScope = new EditorGUI.DisabledScope(_onlyShowKeys);
bool newShowFullShortcutPaths =
EditorGUILayout.Toggle("Show full shortcut paths", _showFullShortcutPaths);
bool newIgnoreKeypressesWithoutShortcut = EditorGUILayout.Toggle(
"Ignore keypresses with no defined shortcut", _ignoreKeypressesWithoutShortcut);
EditorGUIUtility.labelWidth -= 120;
// Store the values only if some change was detected. And also, enable or disable accordingly.
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(KEY_ENABLED, enabled);
if (enabled)
{
Enable();
}
else
{
Disable();
}
EditorPrefs.SetBool(KEY_ONLYSHOWKEYS, newOnlyShowKeys);
EditorPrefs.SetBool(KEY_SHOWFULLSHORTCUTPATHS, newShowFullShortcutPaths);
EditorPrefs.SetBool(KEY_IGNOREKEYPRESSESWITHOUTSHORTCUTS, newIgnoreKeypressesWithoutShortcut);
}
},
keywords = new HashSet<string>(new[] { "Keyboard", "Shortcut", "Keypress", "Keyboard" })
};
return provider;
}
private static void Enable()
{
EditorPrefs.SetBool(KEY_ENABLED, true);
_onlyShowKeys = EditorPrefs.GetBool(KEY_ONLYSHOWKEYS, _onlyShowKeys);
_showFullShortcutPaths = EditorPrefs.GetBool(KEY_SHOWFULLSHORTCUTPATHS, _showFullShortcutPaths);
_ignoreKeypressesWithoutShortcut =
EditorPrefs.GetBool(KEY_IGNOREKEYPRESSESWITHOUTSHORTCUTS, _ignoreKeypressesWithoutShortcut);
_notificationGuiContent.image ??= Resources.Load<Texture2D>("Icons/ICO_Keyboard");
// Use reflection to add our method to Unity's internal input event delegate.
// Why is this thing still not publicly exposed after so may years one can only wonder.
// The only documentation for it is the comment next to its declaration:
// "Global key up/down event that was not handled by anyone"
var globalEventHandlerFieldInfo = typeof(EditorApplication).GetField("globalEventHandler",
BindingFlags.Static | BindingFlags.NonPublic);
var globalEventHandler = (EditorApplication.CallbackFunction)globalEventHandlerFieldInfo.GetValue(null);
globalEventHandler += DisplayCurrentPressedKeyCombination;
globalEventHandlerFieldInfo.SetValue(null, globalEventHandler);
}
private static void Disable()
{
EditorPrefs.SetBool(KEY_ENABLED, false);
var globalEventHandlerFieldInfo = typeof(EditorApplication).GetField("globalEventHandler",
BindingFlags.Static | BindingFlags.NonPublic);
var globalEventHandler = (EditorApplication.CallbackFunction)globalEventHandlerFieldInfo.GetValue(null);
globalEventHandler += DisplayCurrentPressedKeyCombination;
globalEventHandlerFieldInfo.SetValue(null, globalEventHandler);
}
private static void DisplayCurrentPressedKeyCombination()
{
if (EditorWindow.focusedWindow == null)
return;
// Make sure the event is a key press.
var e = Event.current;
if (e == null || !e.isKey || e.keyCode == KeyCode.None)
return;
// We only want to display the pressed keys if the user is pressing a non-modifier key.
var k = e.keyCode;
if (k == KeyCode.LeftControl || k == KeyCode.RightControl ||
k == KeyCode.LeftShift || k == KeyCode.RightShift ||
k == KeyCode.LeftAlt || k == KeyCode.RightAlt ||
k == KeyCode.LeftCommand || k == KeyCode.RightCommand)
{
return;
}
// Create a KeyCombination with the keys pressed in the event.
var isActionKeyPressed = Application.platform == RuntimePlatform.OSXEditor ? e.command : e.control;
var shortcutModifiers = (isActionKeyPressed ? ShortcutModifiers.Action : 0) |
(e.shift ? ShortcutModifiers.Shift : 0) |
(e.alt ? ShortcutModifiers.Alt : 0);
_pressedKeyCombination = new KeyCombination(e.keyCode, shortcutModifiers);
// This generates a string such as "Shift+Alt+A"
_notificationGuiContent.text = _pressedKeyCombination.ToString();
// Let's check the pressed combination against the shortcuts that are defined in the settings.
var shortcutId = FindShortcutForActiveContext(in _pressedKeyCombination);
// If there is no shortcut defined for the pressed keys, then show just the keypress.
if (string.IsNullOrEmpty(shortcutId))
{
if (!_ignoreKeypressesWithoutShortcut)
{
EditorWindow.focusedWindow.ShowNotification(_notificationGuiContent, 0);
}
}
else
{
if (_onlyShowKeys)
{
EditorWindow.focusedWindow.ShowNotification(_notificationGuiContent, 0);
}
else
{
var shownPath = shortcutId;
if (!_showFullShortcutPaths)
{
var splits = shortcutId.Split('/');
shownPath = splits[splits.Length - 1];
}
// Split the path (word wrap) if it's too wide for the notification box.
var wrappedPath = Wrap(shownPath, NOTIFICATION_MAX_CHARS_PER_LINE, new[] { '/', ' ' }, false);
_notificationGuiContent.text += $"\n({wrappedPath})";
EditorWindow.focusedWindow.ShowNotification(_notificationGuiContent, 0);
}
}
}
// ReSharper disable PossibleNullReferenceException
private static string FindShortcutForActiveContext(in KeyCombination keyCombination)
{
// Let's put this whole method in a try-catch block.
// Why do this? Because this method uses heavy reflection. So, it is very possible that something goes wrong,
// for example if Unity's internal code changes between versions.
try
{
// In this method, we will use reflection to finally invoke this method:
// ShortcutIntegration.instance.trigger.m_Directory.FindShortcutEntries()
// First, let's find the method: ShortcutManagement.IDirectory.FindShortcutEntries()
// We will do this only once.
// The class has three method overloads with that name, and we are interested in the one that expects an
// IContextManager as its second parameter.
// The signature of the method is:
// public void FindShortcutEntries(List<KeyCombination> combinationSequence, IContextManager contextManager, List<ShortcutEntry> outputShortcuts)
if (_findShortcutsInternalMethodInfo == null)
{
var contextManagerType =
typeof(ShortcutManager).Assembly.GetType("UnityEditor.ShortcutManagement.IContextManager");
_findShortcutsInternalMethodInfo = typeof(ShortcutManager).Assembly
.GetType("UnityEditor.ShortcutManagement.IDirectory")
.GetMethods()
.First(m => m.Name.Equals("FindShortcutEntries") &&
m.GetParameters()[1].ParameterType == contextManagerType);
}
// Find the static instance of ShortcutController: ShortcutIntegration.instance
_shortcutControllerPropertyInfo ??= typeof(ShortcutManager).Assembly
.GetType("UnityEditor.ShortcutManagement.ShortcutIntegration")
.GetProperty("instance", BindingFlags.Public | BindingFlags.Static);
var shortcutController = _shortcutControllerPropertyInfo.GetValue(null);
// Get the IContextManager instance from: ShortcutIntegration.instance.contextManager
// This is the second value passed as parameter to the method.
var contextManager =
shortcutController.GetType().GetProperty("contextManager").GetValue(shortcutController);
// Find the instance of IDirectory for which the method will be invoked: ShortcutIntegration.instance.trigger.m_Directory
var trigger = shortcutController.GetType()
.GetProperty("trigger")
.GetValue(shortcutController);
var directory = trigger.GetType()
.GetField("m_Directory", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(trigger);
// The third parameter is of type List<ShortcutEntry>.
// Since we cannot access that class, we need to instantiate the list using reflection.
// We will create it only once.
if (_listInternalShortcutEntries == null)
{
var shortcutEntryType = typeof(ShortcutManager).Assembly
.GetType("UnityEditor.ShortcutManagement.ShortcutEntry");
var listShortcutEntryType = typeof(List<>).MakeGenericType(shortcutEntryType);
_listInternalShortcutEntries = (IEnumerable<object>)Activator.CreateInstance(listShortcutEntryType);
}
// Invoke the method.
var keyCombinationList = new List<KeyCombination>(1);
keyCombinationList.Add(keyCombination);
var parameters = new object[] { keyCombinationList, contextManager, _listInternalShortcutEntries };
_findShortcutsInternalMethodInfo.Invoke(directory, parameters);
// Get the first element of the list.
var first = (_listInternalShortcutEntries as IEnumerable<object>)?.First();
if (first != null)
{
return (string)first.GetType().GetProperty("displayName").GetValue(first);
}
}
catch
{
return null;
}
return null;
}
private static string Wrap(string s, int maxCharsPerLine, char[] breakChars, bool removeBreakChars = false)
{
if (s.Length <= maxCharsPerLine)
return s;
var sb = new StringBuilder();
int offset = (removeBreakChars ? 0 : 1);
// First loop setup
int currentBreakIndex = s.IndexOfAny(breakChars);
int previousBreakIndex = 0;
int lastLineBreak = 0;
// Loop until we find no more break chars
while (currentBreakIndex > -1)
{
if (currentBreakIndex - lastLineBreak >= maxCharsPerLine)
{
sb.AppendLine(s.Substring(lastLineBreak, offset + previousBreakIndex - lastLineBreak));
lastLineBreak = 1 + previousBreakIndex;
}
previousBreakIndex = currentBreakIndex;
currentBreakIndex = s.IndexOfAny(breakChars, currentBreakIndex + 1);
}
// Add the last bit, checking for one last possible line break.
currentBreakIndex = s.Length;
if (currentBreakIndex - lastLineBreak >= maxCharsPerLine)
{
sb.AppendLine(s.Substring(lastLineBreak, offset + previousBreakIndex - lastLineBreak));
lastLineBreak = 1 + previousBreakIndex;
}
sb.Append(s.Substring(lastLineBreak, s.Length - lastLineBreak));
return sb.ToString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment