From f4c5d14fec7725844f8ad37aa1b00f908ab58bf7 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Sun, 29 Apr 2018 23:54:10 -0400 Subject: [PATCH 1/9] searchable enum attribute and drawer, first pass --- Assets/SearchableEnum.meta | 8 ++ Assets/SearchableEnum/Editor.meta | 8 ++ .../Editor/SearchableEnumDrawer.cs | 109 ++++++++++++++++++ .../Editor/SearchableEnumDrawer.cs.meta | 11 ++ Assets/SearchableEnum/SearchableEnum.cs | 7 ++ Assets/SearchableEnum/SearchableEnum.cs.meta | 11 ++ Assets/SearchableEnum/TestScript.cs | 11 ++ Assets/SearchableEnum/TestScript.cs.meta | 11 ++ Packages/manifest.json | 4 + ProjectSettings/PresetManager.asset | 6 + ProjectSettings/ProjectVersion.txt | 2 +- 11 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 Assets/SearchableEnum.meta create mode 100644 Assets/SearchableEnum/Editor.meta create mode 100644 Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs create mode 100644 Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs.meta create mode 100644 Assets/SearchableEnum/SearchableEnum.cs create mode 100644 Assets/SearchableEnum/SearchableEnum.cs.meta create mode 100644 Assets/SearchableEnum/TestScript.cs create mode 100644 Assets/SearchableEnum/TestScript.cs.meta create mode 100644 Packages/manifest.json create mode 100644 ProjectSettings/PresetManager.asset 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/Editor.meta b/Assets/SearchableEnum/Editor.meta new file mode 100644 index 0000000..4e37292 --- /dev/null +++ b/Assets/SearchableEnum/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b00926b4d8db49a4ab61958bba0fc7af +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs new file mode 100644 index 0000000..8474fef --- /dev/null +++ b/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs @@ -0,0 +1,109 @@ +using UnityEngine; +using UnityEditor; + +[CustomPropertyDrawer(typeof(SearchableEnum))] +public class SearchableEnumDrawer : UnityEditor.PropertyDrawer +{ + + // TODO: display perfect matches first, making it easier to find things like Keycode.A + // TODO: shorten outer rect based on filter + // TODO: focus text editor on open + // TODO: arrow controls + + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + label = EditorGUI.BeginProperty(position, label, property); + position = EditorGUI.PrefixLabel(position, label); + + if (GUI.Button(position, property.enumDisplayNames[property.enumValueIndex], EditorStyles.popup)) + { + PopupWindow.Show(position, new EnumSelectorWindow(property)); + } + EditorGUI.EndProperty(); + } + + private class EnumSelectorWindow : PopupWindowContent + { + private string filterText = ""; + private SerializedProperty property; + private Vector2 scroll; + private string[] enumNames; + + public EnumSelectorWindow(SerializedProperty property) + { + this.property = property; + enumNames = property.enumDisplayNames; + } + + public override void OnOpen() + { + base.OnOpen(); + EditorApplication.update += Repaint; + } + + public override void OnClose() + { + base.OnClose(); + EditorApplication.update -= Repaint; + } + + private void Repaint() + { + PopupWindow.focusedWindow.Repaint(); + } + + public override Vector2 GetWindowSize() + { + return new Vector2(base.GetWindowSize().x, + Mathf.Min(600, enumNames.Length * 16)); + } + + public override void OnGUI(Rect rect) + { + GUIStyle style = "ToolbarSeachTextField"; + GUIStyle cancel = "ToolbarSeachCancelButton"; + + Rect searchRect = new Rect(rect); + searchRect.height = 16; + searchRect.width -= cancel.fixedWidth; + filterText = GUI.TextField(searchRect, filterText, style); + searchRect.x = searchRect.xMax; + searchRect.width = cancel.fixedWidth; + if (GUI.Button(searchRect, "x", cancel)) + { + filterText = ""; + } + + Rect innerRect = new Rect(rect); + innerRect.height = enumNames.Length * 16; + innerRect.x = 0; + innerRect.y = 0; + rect.y += 16; + scroll = GUI.BeginScrollView(rect, scroll, innerRect); + + rect.y = 0; + rect.height = 16; + + for (int i = 0; i < enumNames.Length; i++) + { + if (string.IsNullOrEmpty(filterText) || enumNames[i].ToLower().Contains(filterText.ToLower())) + { + if (rect.Contains(Event.current.mousePosition)) + { + GUI.Box(rect, ""); + if (Event.current.type == EventType.MouseDown) + { + property.enumValueIndex = i; + property.serializedObject.ApplyModifiedProperties(); + PopupWindow.focusedWindow.Close(); + } + } + GUI.Label(rect, enumNames[i]); + rect.y = rect.yMax; + } + } + GUI.EndScrollView(); + } + } +} diff --git a/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs.meta b/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs.meta new file mode 100644 index 0000000..4146bc6 --- /dev/null +++ b/Assets/SearchableEnum/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/SearchableEnum.cs b/Assets/SearchableEnum/SearchableEnum.cs new file mode 100644 index 0000000..d9cceb6 --- /dev/null +++ b/Assets/SearchableEnum/SearchableEnum.cs @@ -0,0 +1,7 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class SearchableEnum : PropertyAttribute +{ +} diff --git a/Assets/SearchableEnum/SearchableEnum.cs.meta b/Assets/SearchableEnum/SearchableEnum.cs.meta new file mode 100644 index 0000000..624e1e2 --- /dev/null +++ b/Assets/SearchableEnum/SearchableEnum.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/TestScript.cs b/Assets/SearchableEnum/TestScript.cs new file mode 100644 index 0000000..8ce8d87 --- /dev/null +++ b/Assets/SearchableEnum/TestScript.cs @@ -0,0 +1,11 @@ +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +public class TestScript : MonoBehaviour +{ + [SearchableEnum] + public KeyCode KeyCode; + + public KeyCode OtherKeyCode; +} diff --git a/Assets/SearchableEnum/TestScript.cs.meta b/Assets/SearchableEnum/TestScript.cs.meta new file mode 100644 index 0000000..ab60d1b --- /dev/null +++ b/Assets/SearchableEnum/TestScript.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/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 From 7fb6d72e4a1c2afb6752447a4a0da34e3c0426f3 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Tue, 1 May 2018 02:16:25 -0400 Subject: [PATCH 2/9] arrow controls, search bar style, scroll to selection --- .../.idea/indexLayout.xml | 7 + .../Editor/SearchableEnumDrawer.cs | 233 +++++++++++++++--- Assets/SearchableEnum/TestScript.cs | 6 + 3 files changed, 206 insertions(+), 40 deletions(-) create mode 100644 .idea/.idea.UnityQuickTips/.idea/indexLayout.xml 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/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs index 8474fef..a60a3b4 100644 --- a/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs +++ b/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs @@ -1,16 +1,14 @@ using UnityEngine; using UnityEditor; +using System; +using System.Collections.Generic; [CustomPropertyDrawer(typeof(SearchableEnum))] -public class SearchableEnumDrawer : UnityEditor.PropertyDrawer +public class SearchableEnumDrawer : PropertyDrawer { - - // TODO: display perfect matches first, making it easier to find things like Keycode.A - // TODO: shorten outer rect based on filter - // TODO: focus text editor on open - // TODO: arrow controls - - + + // TODO: for keycode, add a button to listen for next keycode + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); @@ -18,22 +16,56 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten if (GUI.Button(position, property.enumDisplayNames[property.enumValueIndex], EditorStyles.popup)) { - PopupWindow.Show(position, new EnumSelectorWindow(property)); + PopupWindow.Show(position, new EnumSelectorWindow(property.enumDisplayNames, property.enumValueIndex, + i=> + { + property.enumValueIndex = i; + property.serializedObject.ApplyModifiedProperties(); + } )); } EditorGUI.EndProperty(); } private class EnumSelectorWindow : PopupWindowContent { + private const float ROW_HEIGHT = 16.0f; + private const float ROW_INDENT = 8.0f; + private string filterText = ""; - private SerializedProperty property; private Vector2 scroll; private string[] enumNames; - - public EnumSelectorWindow(SerializedProperty property) + private Action onSelectionMade; + private int currentIndex; + private int hoverIndex; + + private int scrollToIndex; + private float scrollOffset; + + private readonly List entries = new List(); + + private struct IndexedString { - this.property = property; - enumNames = property.enumDisplayNames; + public int index; + public string text; + public GUIContent guiContent; + + public IndexedString(int index, string text) + { + this.index = index; + this.text = text; + guiContent = new GUIContent(text); + } + } + + public EnumSelectorWindow(string[] names, int currentIndex, Action onSelectionMade) + { + enumNames = names; + this.onSelectionMade = onSelectionMade; + this.currentIndex = currentIndex; + hoverIndex = currentIndex; + scrollToIndex = currentIndex; + OnFilterChanged(); + scrollOffset = GetWindowSize().y - ROW_HEIGHT * 2; } public override void OnOpen() @@ -50,58 +82,179 @@ public override void OnClose() private void Repaint() { - PopupWindow.focusedWindow.Repaint(); + EditorWindow.focusedWindow.Repaint(); } public override Vector2 GetWindowSize() { return new Vector2(base.GetWindowSize().x, - Mathf.Min(600, enumNames.Length * 16)); + Mathf.Min(600, (enumNames.Length * ROW_HEIGHT) + EditorStyles.toolbar.fixedHeight)); } + private void DrawBox(Rect r, Color tint) + { + Color c = GUI.color; + GUI.color = tint; + //Dopesheetkeyframe - very white + //SelectionRect - blueish + //LODSliderRange - white, less alpha + //ProfilerTimelineBar - pure white + GUI.Box(r, "", "SelectionRect"); + GUI.color = c; + } + + private void OnFilterChanged() + { + entries.Clear(); + + for (int i = 0; i < enumNames.Length; i++) + { + // Exact matches show up first + if (String.Equals(enumNames[i], filterText, StringComparison.CurrentCultureIgnoreCase)) + { + entries.Insert(0, new IndexedString(i, enumNames[i])); + } + else if (string.IsNullOrEmpty(filterText) || enumNames[i].ToLower().Contains(filterText.ToLower())) + { + entries.Add(new IndexedString(i, enumNames[i])); + } + } + + scroll = Vector2.zero; + } + 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); + } + + private void HandleKeyboard() + { + if (Event.current.type == EventType.KeyDown) + { + if (Event.current.keyCode == KeyCode.DownArrow) + { + hoverIndex = Mathf.Min(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) + { + onSelectionMade(entries[hoverIndex].index); + EditorWindow.focusedWindow.Close(); + } + + if (Event.current.keyCode == KeyCode.Escape) + { + EditorWindow.focusedWindow.Close(); + } + } + } + + private void DrawSearch(Rect rect) { GUIStyle style = "ToolbarSeachTextField"; GUIStyle cancel = "ToolbarSeachCancelButton"; + GUIStyle cancelEmpty = "ToolbarSeachCancelButtonEmpty"; + //GUIStyle style = "SearchTextField"; + //GUIStyle cancel = "SearchCancelButton"; + + if (Event.current.type == EventType.Repaint) + EditorStyles.toolbar.Draw(rect, false, false, false, false); + + GUI.FocusControl("enumsearchtext"); Rect searchRect = new Rect(rect); - searchRect.height = 16; + searchRect.xMin += 6; + searchRect.xMax -= 6; + searchRect.y += 2; searchRect.width -= cancel.fixedWidth; - filterText = GUI.TextField(searchRect, filterText, style); + GUI.SetNextControlName("enumsearchtext"); + + string newText = GUI.TextField(searchRect, filterText, style); + if (newText != filterText) + { + filterText = newText; + OnFilterChanged(); + } + searchRect.x = searchRect.xMax; searchRect.width = cancel.fixedWidth; - if (GUI.Button(searchRect, "x", cancel)) + if (string.IsNullOrEmpty(filterText)) + GUI.Box(searchRect, GUIContent.none, cancelEmpty); + else if (GUI.Button(searchRect, "x", cancel)) { filterText = ""; + OnFilterChanged(); } + } + + private void DrawRow(Rect rowRect, int i) + { + if (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, entries[i].guiContent); + } + + private void DrawSelectionArea(Rect scrollRect) + { + Rect contentRect = new Rect(0, 0, + scrollRect.width - GUI.skin.verticalScrollbar.fixedWidth, + entries.Count * ROW_HEIGHT); - Rect innerRect = new Rect(rect); - innerRect.height = enumNames.Length * 16; - innerRect.x = 0; - innerRect.y = 0; - rect.y += 16; - scroll = GUI.BeginScrollView(rect, scroll, innerRect); - - rect.y = 0; - rect.height = 16; + scroll = GUI.BeginScrollView(scrollRect, scroll, contentRect); + + Rect rowRect = new Rect(0, 0, scrollRect.width, ROW_HEIGHT); - for (int i = 0; i < enumNames.Length; i++) + for (int i = 0; i < entries.Count; i++) { - if (string.IsNullOrEmpty(filterText) || enumNames[i].ToLower().Contains(filterText.ToLower())) + 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 (rect.Contains(Event.current.mousePosition)) + if (Event.current.type == EventType.MouseMove || + Event.current.type == EventType.ScrollWheel) + hoverIndex = i; + if (Event.current.type == EventType.MouseDown) { - GUI.Box(rect, ""); - if (Event.current.type == EventType.MouseDown) - { - property.enumValueIndex = i; - property.serializedObject.ApplyModifiedProperties(); - PopupWindow.focusedWindow.Close(); - } + onSelectionMade(entries[i].index); + EditorWindow.focusedWindow.Close(); } - GUI.Label(rect, enumNames[i]); - rect.y = rect.yMax; } + + DrawRow(rowRect, i); + + rowRect.y = rowRect.yMax; } GUI.EndScrollView(); } diff --git a/Assets/SearchableEnum/TestScript.cs b/Assets/SearchableEnum/TestScript.cs index 8ce8d87..afe4dec 100644 --- a/Assets/SearchableEnum/TestScript.cs +++ b/Assets/SearchableEnum/TestScript.cs @@ -1,11 +1,17 @@ using System.Collections; using System.Collections.Generic; using UnityEngine; +using UnityEngine.XR; public class TestScript : MonoBehaviour { [SearchableEnum] public KeyCode KeyCode; + + [SearchableEnum] + public XRNode Node; public KeyCode OtherKeyCode; + + } From 0973862d0afab84858b6e4a31dfd820a460bf647 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Tue, 1 May 2018 22:21:05 -0400 Subject: [PATCH 3/9] moving scripts to new folder --- Assets/SearchableEnum/Code.meta | 8 ++++++++ Assets/SearchableEnum/{ => Code}/Editor.meta | 0 .../{ => Code}/Editor/SearchableEnumDrawer.cs | 0 .../{ => Code}/Editor/SearchableEnumDrawer.cs.meta | 0 Assets/SearchableEnum/{ => Code}/SearchableEnum.cs | 0 Assets/SearchableEnum/{ => Code}/SearchableEnum.cs.meta | 0 Assets/SearchableEnum/{ => Code}/TestScript.cs | 0 Assets/SearchableEnum/{ => Code}/TestScript.cs.meta | 0 8 files changed, 8 insertions(+) create mode 100644 Assets/SearchableEnum/Code.meta rename Assets/SearchableEnum/{ => Code}/Editor.meta (100%) rename Assets/SearchableEnum/{ => Code}/Editor/SearchableEnumDrawer.cs (100%) rename Assets/SearchableEnum/{ => Code}/Editor/SearchableEnumDrawer.cs.meta (100%) rename Assets/SearchableEnum/{ => Code}/SearchableEnum.cs (100%) rename Assets/SearchableEnum/{ => Code}/SearchableEnum.cs.meta (100%) rename Assets/SearchableEnum/{ => Code}/TestScript.cs (100%) rename Assets/SearchableEnum/{ => Code}/TestScript.cs.meta (100%) 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/Editor.meta b/Assets/SearchableEnum/Code/Editor.meta similarity index 100% rename from Assets/SearchableEnum/Editor.meta rename to Assets/SearchableEnum/Code/Editor.meta diff --git a/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs similarity index 100% rename from Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs rename to Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs diff --git a/Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs.meta b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs.meta similarity index 100% rename from Assets/SearchableEnum/Editor/SearchableEnumDrawer.cs.meta rename to Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs.meta diff --git a/Assets/SearchableEnum/SearchableEnum.cs b/Assets/SearchableEnum/Code/SearchableEnum.cs similarity index 100% rename from Assets/SearchableEnum/SearchableEnum.cs rename to Assets/SearchableEnum/Code/SearchableEnum.cs diff --git a/Assets/SearchableEnum/SearchableEnum.cs.meta b/Assets/SearchableEnum/Code/SearchableEnum.cs.meta similarity index 100% rename from Assets/SearchableEnum/SearchableEnum.cs.meta rename to Assets/SearchableEnum/Code/SearchableEnum.cs.meta diff --git a/Assets/SearchableEnum/TestScript.cs b/Assets/SearchableEnum/Code/TestScript.cs similarity index 100% rename from Assets/SearchableEnum/TestScript.cs rename to Assets/SearchableEnum/Code/TestScript.cs diff --git a/Assets/SearchableEnum/TestScript.cs.meta b/Assets/SearchableEnum/Code/TestScript.cs.meta similarity index 100% rename from Assets/SearchableEnum/TestScript.cs.meta rename to Assets/SearchableEnum/Code/TestScript.cs.meta From 6c2a095b7d60a4e6ea2ee64af6000a4d9d5c1281 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Tue, 1 May 2018 23:43:59 -0400 Subject: [PATCH 4/9] class reorganization and docs --- .../Code/Editor/SearchableEnumDrawer.cs | 272 ++-------------- .../Code/Editor/SearchablePopup.cs | 292 ++++++++++++++++++ .../Code/Editor/SearchablePopup.cs.meta | 3 + Assets/SearchableEnum/Code/SearchableEnum.cs | 7 - .../Code/SearchableEnumAttribute.cs | 17 + ...s.meta => SearchableEnumAttribute.cs.meta} | 0 .../SearchableEnum/Code/SearchableEnumDemo.cs | 17 + ...ipt.cs.meta => SearchableEnumDemo.cs.meta} | 0 Assets/SearchableEnum/Code/TestScript.cs | 17 - .../SearchableEnum/SearchableEnumDemo.asset | 16 + .../SearchableEnumDemo.asset.meta | 8 + 11 files changed, 376 insertions(+), 273 deletions(-) create mode 100644 Assets/SearchableEnum/Code/Editor/SearchablePopup.cs create mode 100644 Assets/SearchableEnum/Code/Editor/SearchablePopup.cs.meta delete mode 100644 Assets/SearchableEnum/Code/SearchableEnum.cs create mode 100644 Assets/SearchableEnum/Code/SearchableEnumAttribute.cs rename Assets/SearchableEnum/Code/{SearchableEnum.cs.meta => SearchableEnumAttribute.cs.meta} (100%) create mode 100644 Assets/SearchableEnum/Code/SearchableEnumDemo.cs rename Assets/SearchableEnum/Code/{TestScript.cs.meta => SearchableEnumDemo.cs.meta} (100%) delete mode 100644 Assets/SearchableEnum/Code/TestScript.cs create mode 100644 Assets/SearchableEnum/SearchableEnumDemo.asset create mode 100644 Assets/SearchableEnum/SearchableEnumDemo.asset.meta diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs index a60a3b4..841b4bf 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs @@ -1,262 +1,36 @@ -using UnityEngine; -using UnityEditor; +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + using System; -using System.Collections.Generic; +using UnityEditor; +using UnityEngine; -[CustomPropertyDrawer(typeof(SearchableEnum))] -public class SearchableEnumDrawer : PropertyDrawer +namespace RoboRyanTron.SearchableEnum.Editor { - - // TODO: for keycode, add a button to listen for next keycode - - public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - label = EditorGUI.BeginProperty(position, label, property); - position = EditorGUI.PrefixLabel(position, label); - - if (GUI.Button(position, property.enumDisplayNames[property.enumValueIndex], EditorStyles.popup)) - { - PopupWindow.Show(position, new EnumSelectorWindow(property.enumDisplayNames, property.enumValueIndex, - i=> - { - property.enumValueIndex = i; - property.serializedObject.ApplyModifiedProperties(); - } )); - } - EditorGUI.EndProperty(); - } - - private class EnumSelectorWindow : PopupWindowContent + [CustomPropertyDrawer(typeof(SearchableEnumAttribute))] + public class SearchableEnumDrawer : PropertyDrawer { - private const float ROW_HEIGHT = 16.0f; - private const float ROW_INDENT = 8.0f; - - private string filterText = ""; - private Vector2 scroll; - private string[] enumNames; - private Action onSelectionMade; - private int currentIndex; - private int hoverIndex; - - private int scrollToIndex; - private float scrollOffset; - - private readonly List entries = new List(); - - private struct IndexedString - { - public int index; - public string text; - public GUIContent guiContent; - - public IndexedString(int index, string text) - { - this.index = index; - this.text = text; - guiContent = new GUIContent(text); - } - } - - public EnumSelectorWindow(string[] names, int currentIndex, Action onSelectionMade) - { - enumNames = names; - this.onSelectionMade = onSelectionMade; - this.currentIndex = currentIndex; - hoverIndex = currentIndex; - scrollToIndex = currentIndex; - OnFilterChanged(); - scrollOffset = GetWindowSize().y - ROW_HEIGHT * 2; - } - - public override void OnOpen() - { - base.OnOpen(); - EditorApplication.update += Repaint; - } - - public override void OnClose() - { - base.OnClose(); - EditorApplication.update -= Repaint; - } - - private void Repaint() - { - EditorWindow.focusedWindow.Repaint(); - } - - public override Vector2 GetWindowSize() - { - return new Vector2(base.GetWindowSize().x, - Mathf.Min(600, (enumNames.Length * ROW_HEIGHT) + EditorStyles.toolbar.fixedHeight)); - } - - private void DrawBox(Rect r, Color tint) - { - Color c = GUI.color; - GUI.color = tint; - //Dopesheetkeyframe - very white - //SelectionRect - blueish - //LODSliderRange - white, less alpha - //ProfilerTimelineBar - pure white - GUI.Box(r, "", "SelectionRect"); - GUI.color = c; - } - - private void OnFilterChanged() - { - entries.Clear(); - - for (int i = 0; i < enumNames.Length; i++) - { - // Exact matches show up first - if (String.Equals(enumNames[i], filterText, StringComparison.CurrentCultureIgnoreCase)) - { - entries.Insert(0, new IndexedString(i, enumNames[i])); - } - else if (string.IsNullOrEmpty(filterText) || enumNames[i].ToLower().Contains(filterText.ToLower())) - { - entries.Add(new IndexedString(i, enumNames[i])); - } - } - - scroll = Vector2.zero; - } - - 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); - } - - private void HandleKeyboard() - { - if (Event.current.type == EventType.KeyDown) - { - if (Event.current.keyCode == KeyCode.DownArrow) - { - hoverIndex = Mathf.Min(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) - { - onSelectionMade(entries[hoverIndex].index); - EditorWindow.focusedWindow.Close(); - } - - if (Event.current.keyCode == KeyCode.Escape) - { - EditorWindow.focusedWindow.Close(); - } - } - } - - private void DrawSearch(Rect rect) - { - GUIStyle style = "ToolbarSeachTextField"; - GUIStyle cancel = "ToolbarSeachCancelButton"; - GUIStyle cancelEmpty = "ToolbarSeachCancelButtonEmpty"; - //GUIStyle style = "SearchTextField"; - //GUIStyle cancel = "SearchCancelButton"; - - if (Event.current.type == EventType.Repaint) - EditorStyles.toolbar.Draw(rect, false, false, false, false); - - GUI.FocusControl("enumsearchtext"); - - Rect searchRect = new Rect(rect); - searchRect.xMin += 6; - searchRect.xMax -= 6; - searchRect.y += 2; - searchRect.width -= cancel.fixedWidth; - GUI.SetNextControlName("enumsearchtext"); - - string newText = GUI.TextField(searchRect, filterText, style); - if (newText != filterText) - { - filterText = newText; - OnFilterChanged(); - } - - searchRect.x = searchRect.xMax; - searchRect.width = cancel.fixedWidth; - if (string.IsNullOrEmpty(filterText)) - GUI.Box(searchRect, GUIContent.none, cancelEmpty); - else if (GUI.Button(searchRect, "x", cancel)) - { - filterText = ""; - OnFilterChanged(); - } - } - - private void DrawRow(Rect rowRect, int i) - { - if (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, entries[i].guiContent); - } - - private void DrawSelectionArea(Rect scrollRect) + // TODO: for keycode, add a button to listen for next keycode + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { - Rect contentRect = new Rect(0, 0, - scrollRect.width - GUI.skin.verticalScrollbar.fixedWidth, - entries.Count * ROW_HEIGHT); - - scroll = GUI.BeginScrollView(scrollRect, scroll, contentRect); + label = EditorGUI.BeginProperty(position, label, property); + position = EditorGUI.PrefixLabel(position, label); - Rect rowRect = new Rect(0, 0, scrollRect.width, ROW_HEIGHT); - - for (int i = 0; i < entries.Count; i++) + if (GUI.Button(position, property.enumDisplayNames[property.enumValueIndex], EditorStyles.popup)) { - 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)) + Action onSelect = i => { - if (Event.current.type == EventType.MouseMove || - Event.current.type == EventType.ScrollWheel) - hoverIndex = i; - if (Event.current.type == EventType.MouseDown) - { - onSelectionMade(entries[i].index); - EditorWindow.focusedWindow.Close(); - } - } - - DrawRow(rowRect, i); + property.enumValueIndex = i; + property.serializedObject.ApplyModifiedProperties(); + }; - rowRect.y = rowRect.yMax; + SearchablePopup.Show(position, property.enumDisplayNames, + property.enumValueIndex, onSelect); } - GUI.EndScrollView(); + EditorGUI.EndProperty(); } } } diff --git a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs new file mode 100644 index 0000000..396f1ff --- /dev/null +++ b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs @@ -0,0 +1,292 @@ +// ---------------------------------------------------------------------------- +// 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 + { + private const float ROW_HEIGHT = 16.0f; + private const float ROW_INDENT = 8.0f; + + /// 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); + } + + private static void Repaint() + { EditorWindow.focusedWindow.Repaint(); } + + /// + /// Stores a list of strings and can return a subset of that list that + /// matches a given filter string. + /// + private class FilteredList + { + private readonly string[] allItems; + public string Filter { get; private set; } + + public List Entries { get; private set; } + + public FilteredList(string[] allItems) + { + this.allItems = allItems; + Entries = new List(); + UpdateFilter(""); + } + + public struct Entry + { + public int index; + public string text; + } + + 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; + } + } + + private readonly Action onSelectionMade; + private readonly int currentIndex; + private readonly FilteredList list; + + private Vector2 scroll; + private int hoverIndex; + private int scrollToIndex; + private float scrollOffset; + + 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; + } + + public override void OnOpen() + { + base.OnOpen(); + 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); + } + + /// 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, "", "SelectionRect"); + GUI.color = c; + } + + private void DrawSearch(Rect rect) + { + GUIStyle search = "ToolbarSeachTextField"; //SearchTextField + GUIStyle cancel = "ToolbarSeachCancelButton"; //SearchCancelButton + GUIStyle cancelEmpty = "ToolbarSeachCancelButtonEmpty"; + + 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 -= cancel.fixedWidth; + + GUI.FocusControl("enumsearchtext"); + GUI.SetNextControlName("enumsearchtext"); + string newText = GUI.TextField(searchRect, list.Filter, search); + + if (list.UpdateFilter(newText)) + scroll = Vector2.zero; + + searchRect.x = searchRect.xMax; + searchRect.width = cancel.fixedWidth; + + if (string.IsNullOrEmpty(list.Filter)) + GUI.Box(searchRect, GUIContent.none, cancelEmpty); + else if (GUI.Button(searchRect, "x", cancel)) + { + 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) + { + onSelectionMade(list.Entries[hoverIndex].index); + EditorWindow.focusedWindow.Close(); + } + + if (Event.current.keyCode == KeyCode.Escape) + { + EditorWindow.focusedWindow.Close(); + } + } + } + } +} \ 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/SearchableEnum.cs b/Assets/SearchableEnum/Code/SearchableEnum.cs deleted file mode 100644 index d9cceb6..0000000 --- a/Assets/SearchableEnum/Code/SearchableEnum.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; - -public class SearchableEnum : PropertyAttribute -{ -} diff --git a/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs new file mode 100644 index 0000000..7e2a858 --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs @@ -0,0 +1,17 @@ +// ---------------------------------------------------------------------------- +// Author: Ryan Hipple +// Date: 05/01/2018 +// ---------------------------------------------------------------------------- + +using System; +using UnityEngine; + +namespace RoboRyanTron.SearchableEnum +{ + /// + /// An attribute meant to be applied to serializeable enum fields that will + /// display an enum selector that is searchable with a string filter. + /// + [AttributeUsage(AttributeTargets.Field)] + public class SearchableEnumAttribute : PropertyAttribute {} +} diff --git a/Assets/SearchableEnum/Code/SearchableEnum.cs.meta b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs.meta similarity index 100% rename from Assets/SearchableEnum/Code/SearchableEnum.cs.meta rename to Assets/SearchableEnum/Code/SearchableEnumAttribute.cs.meta diff --git a/Assets/SearchableEnum/Code/SearchableEnumDemo.cs b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs new file mode 100644 index 0000000..4d9a71b --- /dev/null +++ b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs @@ -0,0 +1,17 @@ +using UnityEngine; +using UnityEngine.XR; + +namespace RoboRyanTron.SearchableEnum +{ + [CreateAssetMenu] + public class SearchableEnumDemo : ScriptableObject + { + [SearchableEnum] + public KeyCode KeyCode; + + [SearchableEnum] + public XRNode Node; + + public KeyCode OtherKeyCode; + } +} diff --git a/Assets/SearchableEnum/Code/TestScript.cs.meta b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs.meta similarity index 100% rename from Assets/SearchableEnum/Code/TestScript.cs.meta rename to Assets/SearchableEnum/Code/SearchableEnumDemo.cs.meta diff --git a/Assets/SearchableEnum/Code/TestScript.cs b/Assets/SearchableEnum/Code/TestScript.cs deleted file mode 100644 index afe4dec..0000000 --- a/Assets/SearchableEnum/Code/TestScript.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.XR; - -public class TestScript : MonoBehaviour -{ - [SearchableEnum] - public KeyCode KeyCode; - - [SearchableEnum] - public XRNode Node; - - public KeyCode OtherKeyCode; - - -} diff --git a/Assets/SearchableEnum/SearchableEnumDemo.asset b/Assets/SearchableEnum/SearchableEnumDemo.asset new file mode 100644 index 0000000..ab9df3c --- /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: New Test Script + m_EditorClassIdentifier: + KeyCode: 0 + Node: 0 + OtherKeyCode: 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: From fef87bf90c9ba7476e7034dedb8faed4bd9cc434 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Wed, 2 May 2018 00:32:43 -0400 Subject: [PATCH 5/9] using a custom button call so that i can control the controldID and let it work with keyboard focus --- .../Code/Editor/SearchableEnumDrawer.cs | 34 +++++++++++++++++-- .../SearchableEnum/Code/SearchableEnumDemo.cs | 6 ++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs index 841b4bf..b9942fd 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs @@ -16,10 +16,12 @@ public class SearchableEnumDrawer : PropertyDrawer public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + int id = GUIUtility.GetControlID("SearchableEnumDrawer".GetHashCode(), FocusType.Keyboard, Rect.zero); + label = EditorGUI.BeginProperty(position, label, property); - position = EditorGUI.PrefixLabel(position, label); - - if (GUI.Button(position, property.enumDisplayNames[property.enumValueIndex], EditorStyles.popup)) + position = EditorGUI.PrefixLabel(position, id, label); + + if (DropdownButton(id, position, new GUIContent(property.enumDisplayNames[property.enumValueIndex]))) { Action onSelect = i => { @@ -32,5 +34,31 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten } EditorGUI.EndProperty(); } + + 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/SearchableEnumDemo.cs b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs index 4d9a71b..031d352 100644 --- a/Assets/SearchableEnum/Code/SearchableEnumDemo.cs +++ b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs @@ -7,11 +7,11 @@ namespace RoboRyanTron.SearchableEnum public class SearchableEnumDemo : ScriptableObject { [SearchableEnum] - public KeyCode KeyCode; + public KeyCode AwesomeKeyCode; + public KeyCode LameKeyCode; + [SearchableEnum] public XRNode Node; - - public KeyCode OtherKeyCode; } } From 22ec6a29870464f903cb8305bab2abcd62d66772 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Wed, 2 May 2018 23:12:50 -0400 Subject: [PATCH 6/9] error handling for unsupported types --- .../Code/Editor/SearchableEnumDrawer.cs | 25 ++++++++++++++++++- .../Code/SearchableEnumAttribute.cs | 5 ++-- .../SearchableEnum/Code/SearchableEnumDemo.cs | 18 ++++++++----- .../SearchableEnum/SearchableEnumDemo.asset | 6 ++--- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs index b9942fd..172979d 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs @@ -9,13 +9,31 @@ namespace RoboRyanTron.SearchableEnum.Editor { + /// + /// Draws the custom enum selector popup for enum fileds using the + /// SearchableEnumAttribute. + /// [CustomPropertyDrawer(typeof(SearchableEnumAttribute))] public class SearchableEnumDrawer : PropertyDrawer { // TODO: for keycode, add a button to listen for next keycode - + + private const string TYPE_ERROR = + "SearchableEnum can only be used on enum fields."; + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + 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; + } + int id = GUIUtility.GetControlID("SearchableEnumDrawer".GetHashCode(), FocusType.Keyboard, Rect.zero); label = EditorGUI.BeginProperty(position, label, property); @@ -35,6 +53,11 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten 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; diff --git a/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs index 7e2a858..9071dac 100644 --- a/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs +++ b/Assets/SearchableEnum/Code/SearchableEnumAttribute.cs @@ -9,8 +9,9 @@ namespace RoboRyanTron.SearchableEnum { /// - /// An attribute meant to be applied to serializeable enum fields that will - /// display an enum selector that is searchable with a string filter. + /// 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/SearchableEnumDemo.cs b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs index 031d352..be419c4 100644 --- a/Assets/SearchableEnum/Code/SearchableEnumDemo.cs +++ b/Assets/SearchableEnum/Code/SearchableEnumDemo.cs @@ -1,17 +1,23 @@ -using UnityEngine; -using UnityEngine.XR; +// ---------------------------------------------------------------------------- +// 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 { - [SearchableEnum] - public KeyCode AwesomeKeyCode; - + [Tooltip("This enum is fucking miserable.")] public KeyCode LameKeyCode; + [Tooltip("The finest enum browsing experience one can have.")] [SearchableEnum] - public XRNode Node; + public KeyCode AwesomeKeyCode; } } diff --git a/Assets/SearchableEnum/SearchableEnumDemo.asset b/Assets/SearchableEnum/SearchableEnumDemo.asset index ab9df3c..fcaf798 100644 --- a/Assets/SearchableEnum/SearchableEnumDemo.asset +++ b/Assets/SearchableEnum/SearchableEnumDemo.asset @@ -9,8 +9,8 @@ MonoBehaviour: m_Enabled: 1 m_EditorHideFlags: 0 m_Script: {fileID: 11500000, guid: 7234ba95444a32844ac7025034e83bd2, type: 3} - m_Name: New Test Script + m_Name: SearchableEnumDemo m_EditorClassIdentifier: - KeyCode: 0 + AwesomeKeyCode: 97 + LameKeyCode: 273 Node: 0 - OtherKeyCode: 0 From 69618eb24e7194448dd34263c7a7646bee86c688 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Thu, 3 May 2018 22:50:47 -0400 Subject: [PATCH 7/9] fixing out of range exception when a search has no results --- Assets/SearchableEnum/Code/Editor/SearchablePopup.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs index 396f1ff..78c8e9e 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs @@ -181,9 +181,12 @@ private void DrawSearch(Rect rect) GUI.FocusControl("enumsearchtext"); GUI.SetNextControlName("enumsearchtext"); string newText = GUI.TextField(searchRect, list.Filter, search); - + if (list.UpdateFilter(newText)) + { + hoverIndex = 0; scroll = Vector2.zero; + } searchRect.x = searchRect.xMax; searchRect.width = cancel.fixedWidth; @@ -278,8 +281,11 @@ private void HandleKeyboard() if (Event.current.keyCode == KeyCode.Return) { - onSelectionMade(list.Entries[hoverIndex].index); - EditorWindow.focusedWindow.Close(); + if (hoverIndex >= 0 && hoverIndex < list.Entries.Count) + { + onSelectionMade(list.Entries[hoverIndex].index); + EditorWindow.focusedWindow.Close(); + } } if (Event.current.keyCode == KeyCode.Escape) From a7a009bddca49c82d892d97e7d49ea5da2d5cbe8 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Thu, 3 May 2018 22:57:52 -0400 Subject: [PATCH 8/9] documentation --- .../Code/Editor/SearchableEnumDrawer.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs index 172979d..9c16bb5 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchableEnumDrawer.cs @@ -16,13 +16,17 @@ namespace RoboRyanTron.SearchableEnum.Editor [CustomPropertyDrawer(typeof(SearchableEnumAttribute))] public class SearchableEnumDrawer : PropertyDrawer { - // TODO: for keycode, add a button to listen for next keycode - 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"; @@ -34,12 +38,18 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten return; } - int id = GUIUtility.GetControlID("SearchableEnumDrawer".GetHashCode(), FocusType.Keyboard, Rect.zero); + // 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); - - if (DropdownButton(id, position, new GUIContent(property.enumDisplayNames[property.enumValueIndex]))) + + GUIContent buttonText = + new GUIContent(property.enumDisplayNames[property.enumValueIndex]); + if (DropdownButton(id, position, buttonText)) { Action onSelect = i => { From b84d3fe88964312efc2ce68e1333aca8520cd8e5 Mon Sep 17 00:00:00 2001 From: roboryantron Date: Thu, 3 May 2018 23:28:33 -0400 Subject: [PATCH 9/9] documentation and cleanup --- .../Code/Editor/SearchablePopup.cs | 132 +++++++++++++----- 1 file changed, 100 insertions(+), 32 deletions(-) diff --git a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs index 78c8e9e..9916730 100644 --- a/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs +++ b/Assets/SearchableEnum/Code/Editor/SearchablePopup.cs @@ -16,9 +16,18 @@ namespace RoboRyanTron.SearchableEnum.Editor /// 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. @@ -37,33 +46,61 @@ public static void Show(Rect activatorRect, string[] options, int current, Actio 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; - public string Filter { get; private set; } - public List Entries { get; private set; } - - public FilteredList(string[] allItems) + /// Create a new filtered list. + /// All The items to filter. + public FilteredList(string[] items) { - this.allItems = allItems; + allItems = items; Entries = new List(); UpdateFilter(""); } - public struct Entry - { - public int index; - public string text; - } + /// 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; } } @@ -102,16 +139,56 @@ public bool UpdateFilter(string filter) 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); @@ -122,10 +199,13 @@ private SearchablePopup(string[] names, int currentIndex, Action onSelectio 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; } @@ -151,24 +231,11 @@ public override void OnGUI(Rect rect) DrawSearch(searchRect); DrawSelectionArea(scrollRect); } + #endregion -- PopupWindowContent Overrides ---------------------------- - /// 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, "", "SelectionRect"); - GUI.color = c; - } - + #region -- GUI -------------------------------------------------------- private void DrawSearch(Rect rect) { - GUIStyle search = "ToolbarSeachTextField"; //SearchTextField - GUIStyle cancel = "ToolbarSeachCancelButton"; //SearchCancelButton - GUIStyle cancelEmpty = "ToolbarSeachCancelButtonEmpty"; - if (Event.current.type == EventType.Repaint) EditorStyles.toolbar.Draw(rect, false, false, false, false); @@ -176,11 +243,11 @@ private void DrawSearch(Rect rect) searchRect.xMin += 6; searchRect.xMax -= 6; searchRect.y += 2; - searchRect.width -= cancel.fixedWidth; + searchRect.width -= CancelButton.fixedWidth; - GUI.FocusControl("enumsearchtext"); - GUI.SetNextControlName("enumsearchtext"); - string newText = GUI.TextField(searchRect, list.Filter, search); + GUI.FocusControl(SEARCH_CONTROL_NAME); + GUI.SetNextControlName(SEARCH_CONTROL_NAME); + string newText = GUI.TextField(searchRect, list.Filter, SearchBox); if (list.UpdateFilter(newText)) { @@ -189,11 +256,11 @@ private void DrawSearch(Rect rect) } searchRect.x = searchRect.xMax; - searchRect.width = cancel.fixedWidth; + searchRect.width = CancelButton.fixedWidth; if (string.IsNullOrEmpty(list.Filter)) - GUI.Box(searchRect, GUIContent.none, cancelEmpty); - else if (GUI.Button(searchRect, "x", cancel)) + GUI.Box(searchRect, GUIContent.none, DisabledCancelButton); + else if (GUI.Button(searchRect, "x", CancelButton)) { list.UpdateFilter(""); scroll = Vector2.zero; @@ -294,5 +361,6 @@ private void HandleKeyboard() } } } + #endregion -- GUI ----------------------------------------------------- } } \ No newline at end of file