diff --git a/.idea/.idea.UnityQuickTips/.idea/indexLayout.xml b/.idea/.idea.UnityQuickTips/.idea/indexLayout.xml new file mode 100644 index 0000000..f1feadf --- /dev/null +++ b/.idea/.idea.UnityQuickTips/.idea/indexLayout.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Assets/SearchableEnum.meta b/Assets/SearchableEnum.meta new file mode 100644 index 0000000..ed2fde5 --- /dev/null +++ b/Assets/SearchableEnum.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d0a31054c67400e48b3130f94fbb12cd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Code.meta b/Assets/SearchableEnum/Code.meta new file mode 100644 index 0000000..c03d3b2 --- /dev/null +++ b/Assets/SearchableEnum/Code.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 45ca3fd6dbb69a4408a782f191a3d61e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Code/Editor.meta b/Assets/SearchableEnum/Code/Editor.meta new file mode 100644 index 0000000..4e37292 --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b00926b4d8db49a4ab61958bba0fc7af +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs new file mode 100644 index 0000000..9c16bb5 --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs @@ -0,0 +1,97 @@ +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + +using System; +using UnityEditor; +using UnityEngine; + +namespace RoboRyanTron.SearchableEnum.Editor +{ + /// + /// Draws the custom enum selector popup for enum fileds using the + /// SearchableEnumAttribute. + /// + [CustomPropertyDrawer(typeof(SearchableEnumAttribute))] + public class SearchableEnumDrawer : PropertyDrawer + { + private const string TYPE_ERROR = + "SearchableEnum can only be used on enum fields."; + + /// + /// Cache of the hash to use to resolve the ID for the drawer. + /// + private int idHash; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + // If this is not used on an eunum, show an error + if (property.type != "Enum") + { + GUIStyle errorStyle = "CN EntryErrorIconSmall"; + Rect r = new Rect(position); + r.width = errorStyle.fixedWidth; + position.xMin = r.xMax; + GUI.Label(r, "", errorStyle); + GUI.Label(position, TYPE_ERROR); + return; + } + + // By manually creating the control ID, we can keep the ID for the + // label and button the same. This lets them be selected together + // with the keyboard in the inspector, much like a normal popup. + if (idHash == 0) idHash = "SearchableEnumDrawer".GetHashCode(); + int id = GUIUtility.GetControlID(idHash, FocusType.Keyboard, position); + + label = EditorGUI.BeginProperty(position, label, property); + position = EditorGUI.PrefixLabel(position, id, label); + + GUIContent buttonText = + new GUIContent(property.enumDisplayNames[property.enumValueIndex]); + if (DropdownButton(id, position, buttonText)) + { + Action onSelect = i => + { + property.enumValueIndex = i; + property.serializedObject.ApplyModifiedProperties(); + }; + + SearchablePopup.Show(position, property.enumDisplayNames, + property.enumValueIndex, onSelect); + } + EditorGUI.EndProperty(); + } + + /// + /// A custom button drawer that allows for a controlID so that we can + /// sync the button ID and the label ID to allow for keyboard + /// navigation like the built-in enum drawers. + /// + private static bool DropdownButton(int id, Rect position, GUIContent content) + { + Event current = Event.current; + switch (current.type) + { + case EventType.MouseDown: + if (position.Contains(current.mousePosition) && current.button == 0) + { + Event.current.Use(); + return true; + } + break; + case EventType.KeyDown: + if (GUIUtility.keyboardControl == id && current.character =='\n') + { + Event.current.Use(); + return true; + } + break; + case EventType.Repaint: + EditorStyles.popup.Draw(position, content, id, false); + break; + } + return false; + } + } +} diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs.meta b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs.meta new file mode 100644 index 0000000..4146bc6 --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 82586c3ea06182a4999528926f6e441b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs new file mode 100644 index 0000000..9916730 --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs @@ -0,0 +1,366 @@ +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using UnityEditor; +using UnityEngine; + +namespace RoboRyanTron.SearchableEnum.Editor +{ + /// + /// A popup window that displays a list of options and may use a search + /// string to filter the displayed content. + /// + public class SearchablePopup : PopupWindowContent + { + #region -- Constants -------------------------------------------------- + /// Height of each element in the popup list. + private const float ROW_HEIGHT = 16.0f; + + /// How far to indent list entries. + private const float ROW_INDENT = 8.0f; + + /// Name to use for the text field for search. + private const string SEARCH_CONTROL_NAME = "EnumSearchText"; + #endregion -- Constants ----------------------------------------------- + + #region -- Static Functions ------------------------------------------- + /// Show a new SearchablePopup. + /// + /// Rectangle of the button that triggered the popup. + /// + /// List of strings to choose from. + /// + /// Index of the currently selected string. + /// + /// + /// Callback to trigger when a choice is made. + /// + public static void Show(Rect activatorRect, string[] options, int current, Action onSelectionMade) + { + SearchablePopup win = + new SearchablePopup(options, current, onSelectionMade); + PopupWindow.Show(activatorRect, win); + } + + /// + /// Force the focused window to redraw. This can be used to make the + /// popup more responsive to mouse movement. + /// + private static void Repaint() + { EditorWindow.focusedWindow.Repaint(); } + + /// Draw a generic box. + /// Where to draw. + /// Color to tint the box. + private static void DrawBox(Rect rect, Color tint) + { + Color c = GUI.color; + GUI.color = tint; + GUI.Box(rect, "", Selection); + GUI.color = c; + } + #endregion -- Static Functions ---------------------------------------- + + #region -- Helper Classes --------------------------------------------- + /// + /// Stores a list of strings and can return a subset of that list that + /// matches a given filter string. + /// + private class FilteredList + { + /// + /// An entry in the filtererd list, mapping the text to the + /// original index. + /// + public struct Entry + { + public int index; + public string text; + } + + /// All posibile items in the list. + private readonly string[] allItems; + + /// Create a new filtered list. + /// All The items to filter. + public FilteredList(string[] items) + { + allItems = items; + Entries = new List(); + UpdateFilter(""); + } + + /// The current string filtering the list. + public string Filter { get; private set; } + + /// All valid entries for the current filter. + public List Entries { get; private set; } + + /// Total possible entries in the list. + public int MaxLength + { get { return allItems.Length; } } + + /// + /// Sets a new filter string and updates the Entries that match the + /// new filter if it has changed. + /// + /// String to use to filter the list. + /// + /// True if the filter is updated, false if newFilter is the same + /// as the current Filter and no update is necessary. + /// + public bool UpdateFilter(string filter) + { + if (Filter == filter) + return false; + + Filter = filter; + Entries.Clear(); + + for (int i = 0; i < allItems.Length; i++) + { + if (string.IsNullOrEmpty(Filter) || allItems[i].ToLower().Contains(Filter.ToLower())) + { + Entry entry = new Entry + { + index = i, + text = allItems[i] + }; + if (string.Equals(allItems[i], Filter, StringComparison.CurrentCultureIgnoreCase)) + Entries.Insert(0, entry); + else + Entries.Add(entry); + } + } + return true; + } + } + #endregion -- Helper Classes ------------------------------------------ + + #region -- Private Variables ------------------------------------------ + /// Callback to trigger when an item is selected. + private readonly Action onSelectionMade; + + /// + /// Index of the item that was selected when the list was opened. + /// + private readonly int currentIndex; + + /// + /// Container for all available options that does the actual string + /// filtering of the content. + /// + private readonly FilteredList list; + + /// Scroll offset for the vertical scroll area. + private Vector2 scroll; + + /// + /// Index of the item under the mouse or selected with the keyboard. + /// + private int hoverIndex; + + /// + /// An item index to scroll to on the next draw. + /// + private int scrollToIndex; + + /// + /// An offset to apply after scrolling to scrollToIndex. This can be + /// used to control if the selection appears at the top, bottom, or + /// center of the popup. + /// + private float scrollOffset; + #endregion -- Private Variables --------------------------------------- + + #region -- GUI Styles ------------------------------------------------- + // GUIStyles implicitly cast from a string. This triggers a lookup into + // the current skin which will be the editor skin and lets us get some + // built-in styles. + + private static GUIStyle SearchBox = "ToolbarSeachTextField"; + private static GUIStyle CancelButton = "ToolbarSeachCancelButton"; + private static GUIStyle DisabledCancelButton = "ToolbarSeachCancelButtonEmpty"; + private static GUIStyle Selection = "SelectionRect"; + #endregion -- GUI Styles ---------------------------------------------- + + #region -- Initialization --------------------------------------------- + private SearchablePopup(string[] names, int currentIndex, Action onSelectionMade) + { + list = new FilteredList(names); + this.currentIndex = currentIndex; + this.onSelectionMade = onSelectionMade; + + hoverIndex = currentIndex; + scrollToIndex = currentIndex; + scrollOffset = GetWindowSize().y - ROW_HEIGHT * 2; + } + #endregion -- Initialization ------------------------------------------ + + #region -- PopupWindowContent Overrides ------------------------------- + public override void OnOpen() + { + base.OnOpen(); + // Force a repaint every frame to be responsive to mouse hover. + EditorApplication.update += Repaint; + } + + public override void OnClose() + { + base.OnClose(); + EditorApplication.update -= Repaint; + } + + public override Vector2 GetWindowSize() + { + return new Vector2(base.GetWindowSize().x, + Mathf.Min(600, list.MaxLength * ROW_HEIGHT + + EditorStyles.toolbar.fixedHeight)); + } + + public override void OnGUI(Rect rect) + { + Rect searchRect = new Rect(0, 0, rect.width, EditorStyles.toolbar.fixedHeight); + Rect scrollRect = Rect.MinMaxRect(0, searchRect.yMax, rect.xMax, rect.yMax); + + HandleKeyboard(); + DrawSearch(searchRect); + DrawSelectionArea(scrollRect); + } + #endregion -- PopupWindowContent Overrides ---------------------------- + + #region -- GUI -------------------------------------------------------- + private void DrawSearch(Rect rect) + { + if (Event.current.type == EventType.Repaint) + EditorStyles.toolbar.Draw(rect, false, false, false, false); + + Rect searchRect = new Rect(rect); + searchRect.xMin += 6; + searchRect.xMax -= 6; + searchRect.y += 2; + searchRect.width -= CancelButton.fixedWidth; + + GUI.FocusControl(SEARCH_CONTROL_NAME); + GUI.SetNextControlName(SEARCH_CONTROL_NAME); + string newText = GUI.TextField(searchRect, list.Filter, SearchBox); + + if (list.UpdateFilter(newText)) + { + hoverIndex = 0; + scroll = Vector2.zero; + } + + searchRect.x = searchRect.xMax; + searchRect.width = CancelButton.fixedWidth; + + if (string.IsNullOrEmpty(list.Filter)) + GUI.Box(searchRect, GUIContent.none, DisabledCancelButton); + else if (GUI.Button(searchRect, "x", CancelButton)) + { + list.UpdateFilter(""); + scroll = Vector2.zero; + } + } + + private void DrawSelectionArea(Rect scrollRect) + { + Rect contentRect = new Rect(0, 0, + scrollRect.width - GUI.skin.verticalScrollbar.fixedWidth, + list.Entries.Count * ROW_HEIGHT); + + scroll = GUI.BeginScrollView(scrollRect, scroll, contentRect); + + Rect rowRect = new Rect(0, 0, scrollRect.width, ROW_HEIGHT); + + for (int i = 0; i < list.Entries.Count; i++) + { + if (scrollToIndex == i && + (Event.current.type == EventType.Repaint + || Event.current.type == EventType.Layout)) + { + Rect r = new Rect(rowRect); + r.y += scrollOffset; + GUI.ScrollTo(r); + scrollToIndex = -1; + scroll.x = 0; + } + + if (rowRect.Contains(Event.current.mousePosition)) + { + if (Event.current.type == EventType.MouseMove || + Event.current.type == EventType.ScrollWheel) + hoverIndex = i; + if (Event.current.type == EventType.MouseDown) + { + onSelectionMade(list.Entries[i].index); + EditorWindow.focusedWindow.Close(); + } + } + + DrawRow(rowRect, i); + + rowRect.y = rowRect.yMax; + } + + GUI.EndScrollView(); + } + + private void DrawRow(Rect rowRect, int i) + { + if (list.Entries[i].index == currentIndex) + DrawBox(rowRect, Color.cyan); + else if (i == hoverIndex) + DrawBox(rowRect, Color.white); + + Rect labelRect = new Rect(rowRect); + labelRect.xMin += ROW_INDENT; + + GUI.Label(labelRect, list.Entries[i].text); + } + + /// + /// Process keyboard input to navigate the choices or make a selection. + /// + private void HandleKeyboard() + { + if (Event.current.type == EventType.KeyDown) + { + if (Event.current.keyCode == KeyCode.DownArrow) + { + hoverIndex = Mathf.Min(list.Entries.Count - 1, hoverIndex + 1); + Event.current.Use(); + scrollToIndex = hoverIndex; + scrollOffset = ROW_HEIGHT; + } + + if (Event.current.keyCode == KeyCode.UpArrow) + { + hoverIndex = Mathf.Max(0, hoverIndex - 1); + Event.current.Use(); + scrollToIndex = hoverIndex; + scrollOffset = -ROW_HEIGHT; + } + + if (Event.current.keyCode == KeyCode.Return) + { + if (hoverIndex >= 0 && hoverIndex < list.Entries.Count) + { + onSelectionMade(list.Entries[hoverIndex].index); + EditorWindow.focusedWindow.Close(); + } + } + + if (Event.current.keyCode == KeyCode.Escape) + { + EditorWindow.focusedWindow.Close(); + } + } + } + #endregion -- GUI ----------------------------------------------------- + } +} \ No newline at end of file diff --git a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs.meta b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs.meta new file mode 100644 index 0000000..7a37faf --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 50e2e887cad444af9c64f4e7de140858 +timeCreated: 1525227782 \ No newline at end of file diff --git a/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs new file mode 100644 index 0000000..9071dac --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs @@ -0,0 +1,18 @@ +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + +using System; +using UnityEngine; + +namespace RoboRyanTron.SearchableEnum +{ + /// + /// Put this attribute on a public (or SerialzeField) enum in a + /// MonoBehaviour or ScriptableObject to get an improved enum selector + /// popup. The enum list is scrollable and can be filtered by typing. + /// + [AttributeUsage(AttributeTargets.Field)] + public class SearchableEnumAttribute : PropertyAttribute {} +} diff --git a/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs.meta b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs.meta new file mode 100644 index 0000000..624e1e2 --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5d57bf0a9f2e56043b171bac62dcd313 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Code/SearchableEnumDemo.cs b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs new file mode 100644 index 0000000..be419c4 --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs @@ -0,0 +1,23 @@ +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + +using UnityEngine; + +namespace RoboRyanTron.SearchableEnum +{ + /// + /// A demo of the SearchableEnumPopup on a ScriptableObject. + /// + [CreateAssetMenu] + public class SearchableEnumDemo : ScriptableObject + { + [Tooltip("This enum is fucking miserable.")] + public KeyCode LameKeyCode; + + [Tooltip("The finest enum browsing experience one can have.")] + [SearchableEnum] + public KeyCode AwesomeKeyCode; + } +} diff --git a/Assets/SearchableEnum/Code/SearchableEnumDemo.cs.meta b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs.meta new file mode 100644 index 0000000..ab60d1b --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7234ba95444a32844ac7025034e83bd2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/SearchableEnumDemo.asset b/Assets/SearchableEnum/SearchableEnumDemo.asset new file mode 100644 index 0000000..fcaf798 --- /dev/null +++ b/Assets/SearchableEnum/SearchableEnumDemo.asset @@ -0,0 +1,16 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_PrefabParentObject: {fileID: 0} + m_PrefabInternal: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7234ba95444a32844ac7025034e83bd2, type: 3} + m_Name: SearchableEnumDemo + m_EditorClassIdentifier: + AwesomeKeyCode: 97 + LameKeyCode: 273 + Node: 0 diff --git a/Assets/SearchableEnum/SearchableEnumDemo.asset.meta b/Assets/SearchableEnum/SearchableEnumDemo.asset.meta new file mode 100644 index 0000000..2aa9194 --- /dev/null +++ b/Assets/SearchableEnum/SearchableEnumDemo.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b0179ebdc142a954aa7579c2c09de6b9 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/manifest.json b/Packages/manifest.json new file mode 100644 index 0000000..526aca6 --- /dev/null +++ b/Packages/manifest.json @@ -0,0 +1,4 @@ +{ + "dependencies": { + } +} diff --git a/ProjectSettings/PresetManager.asset b/ProjectSettings/PresetManager.asset new file mode 100644 index 0000000..636a595 --- /dev/null +++ b/ProjectSettings/PresetManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + m_DefaultList: [] diff --git a/ProjectSettings/ProjectVersion.txt b/ProjectSettings/ProjectVersion.txt index a211ccd..b55040d 100644 --- a/ProjectSettings/ProjectVersion.txt +++ b/ProjectSettings/ProjectVersion.txt @@ -1 +1 @@ -m_EditorVersion: 2017.1.1f1 +m_EditorVersion: 2018.1.0f1