// UltEvents // Copyright 2020 Kybernetik // // Copied from Kybernetik.Core. #if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using UnityEditor; using UnityEngine; namespace UltEvents.Editor { /// [Editor-Only] Allows you to draw GUI fields which can be used to pick an object from a list. public static class ObjectPicker { /************************************************************************************************************************/ #region Main Drawing Methods /************************************************************************************************************************/ /// Draws a field which lets you pick an object from a list and returns the selected object. public static T Draw(Rect area, T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop, GUIStyle style) { var id = CheckCommand(ref selected); if (GUI.Button(area, getLabel(selected), style)) ObjectPickerWindow.Show(id, selected, getOptions(), suggestions, getLabel); CheckDragAndDrop(area, ref selected, getOptions, getDragAndDrop); return selected; } /// Draws a field which lets you pick an object from a list and returns the selected object. public static T Draw(Rect area, T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop) { return Draw(area, selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle); } /************************************************************************************************************************/ /// Draws a field (using GUILayout) which lets you pick an object from a list and returns the selected object. public static T DrawLayout(T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop, GUIStyle style, params GUILayoutOption[] layoutOptions) { var id = CheckCommand(ref selected); if (GUILayout.Button(getLabel(selected), style, layoutOptions)) ObjectPickerWindow.Show(id, selected, getOptions(), suggestions, getLabel); CheckDragAndDrop(GUILayoutUtility.GetLastRect(), ref selected, getOptions, getDragAndDrop); return selected; } /// Draws a field (using GUILayout) which lets you pick an object from a list and returns the selected object. public static T DrawLayout(T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop, params GUILayoutOption[] layoutOptions) { return DrawLayout(selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle, layoutOptions); } /************************************************************************************************************************/ /// /// Draws a field (as an inspector field using GUILayout) which lets you pick an object from a list and returns /// the selected object. /// public static T DrawEditorLayout(GUIContent label, T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop, GUIStyle style, params GUILayoutOption[] layoutOptions) { GUILayout.BeginHorizontal(); { GUILayout.Label(label, GUILayout.Width(EditorGUIUtility.labelWidth - 4)); selected = DrawLayout(selected, getOptions, suggestions, getLabel, getDragAndDrop, style, layoutOptions); } GUILayout.EndHorizontal(); return selected; } /// /// Draws a field (as an inspector field using GUILayout) which lets you pick an object from a list and returns /// the selected object. /// public static T DrawEditorLayout(GUIContent label, T selected, Func> getOptions, int suggestions, Func getLabel, Func getDragAndDrop, params GUILayoutOption[] options) { return DrawEditorLayout(label, selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle, options); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Type Field /************************************************************************************************************************/ /// Draws a field which lets you pick a from a list and returns the selected type. public static Type DrawTypeField(Rect area, Type selected, Func> getOptions, int suggestions, GUIStyle style) { return Draw(area, selected, getOptions, suggestions, getLabel: (type) => new GUIContent(type != null ? type.GetNameCS() : "null"), getDragAndDrop: () => DragAndDrop.objectReferences[0].GetType(), style: style); } /************************************************************************************************************************/ /// Draws a field which lets you pick an asset from a list and returns the selected type. public static Type DrawAssetTypeField(Rect area, Type selected, Func> getOptions, int suggestions, GUIStyle style) { return Draw(area, selected, getOptions, suggestions, getLabel: (type) => new GUIContent(type != null ? type.GetNameCS() : "null", AssetPreview.GetMiniTypeThumbnail(type)), getDragAndDrop: () => DragAndDrop.objectReferences[0].GetType(), style: style); } /************************************************************************************************************************/ /// Draws a field which lets you pick a from a list and returns the selected . public static string DrawTypeField(Rect area, string selectedTypeName, Func> getOptions, int suggestions, GUIStyle style) { var selected = Type.GetType(selectedTypeName); selected = Draw(area, selected, getOptions, suggestions, getLabel: (type) => new GUIContent(type != null ? type.GetNameCS() : "No Type Selected"), getDragAndDrop: () => DragAndDrop.objectReferences[0].GetType(), style: style); return selected != null ? selected.AssemblyQualifiedName : null; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Utils /************************************************************************************************************************/ /// /// Removes any duplicates of the first few elements in `options` (from 0 to `suggestions`) from anywhere later /// in the list. /// public static void RemoveDuplicateSuggestions(List options, int suggestions) where T : class { for (int i = options.Count - 1; i >= suggestions; i--) { var obj = options[i]; for (int j = 0; j < suggestions; j++) { if (obj == options[j]) { options.RemoveAt(j); break; } } } } /************************************************************************************************************************/ private static int CheckCommand(ref T selected) { var id = GUIUtility.GetControlID(FocusType.Passive); ObjectPickerWindow.TryGetPickedObject(id, ref selected); return id; } /************************************************************************************************************************/ private static void CheckDragAndDrop(Rect area, ref T selected, Func> getOptions, Func getDragAndDrop) { var currentEvent = Event.current; if (DragAndDrop.objectReferences.Length == 1 && area.Contains(currentEvent.mousePosition)) { var drop = getDragAndDrop(); // If the dragged object is a valid type, continue. if (!getOptions().Contains(drop)) return; if (currentEvent.type == EventType.DragUpdated || currentEvent.type == EventType.MouseDrag) { DragAndDrop.visualMode = DragAndDropVisualMode.Link; } else if (currentEvent.type == EventType.DragPerform) { selected = drop; DragAndDrop.AcceptDrag(); GUI.changed = true; } } } /************************************************************************************************************************/ private static class InternalGUI { public static readonly GUIStyle TypeButtonStyle; static InternalGUI() { TypeButtonStyle = new GUIStyle(EditorStyles.miniButton) { alignment = TextAnchor.MiddleLeft }; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } /************************************************************************************************************************/ internal sealed class ObjectPickerWindow : EditorWindow { /************************************************************************************************************************/ private static int _FieldID; private static bool _HasPickedObject; private static object _PickedObject; private readonly List Labels = new List(), SearchedLabels = new List(); private readonly List SearchedObjects = new List(); [NonSerialized] private object _SelectedObject; [NonSerialized] private IList _Objects; [NonSerialized] private int _Suggestions; [NonSerialized] private int _LabelWidthCalculationProgress; [NonSerialized] private float _MaxLabelWidth; [NonSerialized] private string _SearchText = ""; [NonSerialized] private Vector2 _ScrollPosition; /************************************************************************************************************************/ private bool HasSearchText { get { return !string.IsNullOrEmpty(_SearchText); } } /************************************************************************************************************************/ public static void Show(int fieldID, T selected, List objects, int suggestions, Func getLabel) { if (objects == null || objects.Count == 0) { Debug.LogError("'objects' list is null or empty."); return; } _FieldID = fieldID; _HasPickedObject = false; var window = CreateInstance(); window.titleContent = new GUIContent("Pick a " + typeof(T).GetNameCS()); window.minSize = new Vector2(112, 100); if (window.Labels.Capacity < objects.Count) window.Labels.Capacity = objects.Count; for (int i = 0; i < objects.Count; i++) window.Labels.Add(getLabel(objects[i])); //Debug.LogTemp("Showing Object Picker Window: " + window._Labels.DeepToString()); window._SelectedObject = selected; window._Objects = objects; window._Suggestions = suggestions; // Auto-Scroll to the selected object. if (selected != null) { object sel = selected; for (int i = 0; i < window._Objects.Count; i++) { if (sel == window._Objects[i]) { window._ScrollPosition = new Vector2(0, i * InternalGUI.LabelHeight); break; } } } //window._Objects.LogErrorIfModified("the '" + nameof(objects) + "' list passed into " + Reflection.GetCallingMethod(2).GetNameCS()); window.ShowAuxWindow(); } /************************************************************************************************************************/ public static void TryGetPickedObject(int fieldID, ref T picked) { if (_HasPickedObject && _FieldID == fieldID) { picked = (T)_PickedObject; _PickedObject = null; _HasPickedObject = false; GUI.changed = true; } } /************************************************************************************************************************/ private void PickAndClose() { _PickedObject = _SelectedObject; _HasPickedObject = true; Close(); UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); } /************************************************************************************************************************/ private void OnGUI() { switch (Event.current.type) { case EventType.MouseMove: case EventType.Layout: case EventType.DragUpdated: case EventType.DragPerform: case EventType.DragExited: case EventType.Ignore: case EventType.Used: case EventType.ValidateCommand: case EventType.ExecuteCommand: case EventType.ContextClick: return; default: break; } if (CheckInput()) { Event.current.Use(); return; } UpdateLabelWidthCalculation(); var area = new Rect(0, 0, position.width, position.height); DrawSearchBar(ref area); area.yMax = position.height; var viewRect = CalculateViewRect(area.height); // Selection List. _ScrollPosition = GUI.BeginScrollView(area, _ScrollPosition, viewRect); { // Figure out how many fields are actually visible. int firstVisibleField, lastVisibleField; DetermineVisibleRange(out firstVisibleField, out lastVisibleField); if (HasSearchText)// Active Search. { DrawSearchedOptions(viewRect, firstVisibleField, lastVisibleField); } else// No Search. { DrawAllOptions(viewRect, firstVisibleField, lastVisibleField); } } GUI.EndScrollView(true); } /************************************************************************************************************************/ private void UpdateLabelWidthCalculation() { if (_LabelWidthCalculationProgress < Labels.Count) { var calculationCount = 0; do { var label = Labels[_LabelWidthCalculationProgress]; var width = InternalGUI.ButtonStyle.CalcSize(label).x; if (_MaxLabelWidth < width) _MaxLabelWidth = width; } while (++_LabelWidthCalculationProgress < Labels.Count && calculationCount++ < 100); Repaint(); } } /************************************************************************************************************************/ private bool CheckInput() { var currentEvent = Event.current; if (currentEvent.type == EventType.KeyUp) { switch (currentEvent.keyCode) { case KeyCode.Return: PickAndClose(); return true; case KeyCode.Escape: Close(); return true; case KeyCode.UpArrow: OffsetSelectedIndex(-1); return true; case KeyCode.DownArrow: OffsetSelectedIndex(1); return true; default: break; } } return false; } /************************************************************************************************************************/ private void DrawSearchBar(ref Rect area) { area.height = InternalGUI.SearchBarHeight; GUI.BeginGroup(area, EditorStyles.toolbar); { area.x += 2; area.y += 2; area.width -= InternalGUI.SearchBarEndStyle.fixedWidth + 4; GUI.SetNextControlName("SearchFilter"); EditorGUI.BeginChangeCheck(); var searchText = GUI.TextField(area, _SearchText, InternalGUI.SearchBarStyle); if (EditorGUI.EndChangeCheck()) OnSearchTextChanged(searchText); EditorGUI.FocusTextInControl("SearchFilter"); area.x = area.xMax; area.width = InternalGUI.SearchBarEndStyle.fixedWidth; if (HasSearchText) { if (GUI.Button(area, "", InternalGUI.SearchBarCancelStyle)) { _SearchText = ""; } } else GUI.Box(area, "", InternalGUI.SearchBarEndStyle); } GUI.EndGroup(); area.x = 0; area.width = position.width; area.y += area.height; } /************************************************************************************************************************/ private void OnSearchTextChanged(string text) { if (string.IsNullOrEmpty(text)) { SearchedLabels.Clear(); SearchedObjects.Clear(); } // If the search text starts the same as before, it will include only a subset of the previous options. // So we can just remove objects from the previous search list instead of checking the full list again. else if (SearchedLabels.Count > 0 && text.StartsWith(_SearchText)) { for (int i = SearchedLabels.Count - 1; i >= 0; i--) { if (!IsVisibleInSearch(text, SearchedLabels[i].text)) { SearchedLabels.RemoveAt(i); SearchedObjects.RemoveAt(i); } } } // Otherwise clear the search list and re-gather any visible objects from the full list. else { SearchedLabels.Clear(); SearchedObjects.Clear(); for (int i = 0; i < Labels.Count; i++) { var label = Labels[i]; if (IsVisibleInSearch(text, label.text)) { SearchedLabels.Add(label); SearchedObjects.Add(_Objects[i]); } } } _SearchText = text; if (!SearchedObjects.Contains(_SelectedObject)) _SelectedObject = SearchedObjects.Count > 0 ? SearchedObjects[0] : null; } private static bool IsVisibleInSearch(string search, string text) { return CultureInfo.CurrentCulture.CompareInfo.IndexOf(text, search, CompareOptions.IgnoreCase) >= 0; } /************************************************************************************************************************/ private Rect CalculateViewRect(float height) { var area = new Rect(); if (HasSearchText) { area.height = InternalGUI.LabelHeight * SearchedLabels.Count; } else { area.height = InternalGUI.LabelHeight * Labels.Count; if (_Suggestions > 0) area.height += InternalGUI.HeaddingStyle.fixedHeight * 2; } if (_MaxLabelWidth < position.width) { area.width = position.width; if (area.height > height) area.width -= 16; } else area.width = _MaxLabelWidth; return area; } /************************************************************************************************************************/ private void DetermineVisibleRange(out int firstVisibleField, out int lastVisibleField) { var top = _ScrollPosition.y; var bottom = top + position.height - InternalGUI.SearchBarHeight; if (_Suggestions > 0) { top -= InternalGUI.HeaddingStyle.fixedHeight * 2; bottom += InternalGUI.HeaddingStyle.fixedHeight; } firstVisibleField = Mathf.Max(0, (int)(top / InternalGUI.LabelHeight)); lastVisibleField = Mathf.Min(Labels.Count, Mathf.CeilToInt(bottom / InternalGUI.LabelHeight)); } /************************************************************************************************************************/ private void DrawAllOptions(Rect area, int firstVisibleField, int lastVisibleField) { if (_Suggestions == 0 || _Suggestions >= Labels.Count) { area.y = firstVisibleField * InternalGUI.LabelHeight; DrawRange(ref area, Labels, _Objects, firstVisibleField, lastVisibleField); } else { area.height = InternalGUI.HeaddingStyle.fixedHeight; GUI.Label(area, "Suggestions", InternalGUI.HeaddingStyle); area.y = area.yMax + firstVisibleField * InternalGUI.LabelHeight; DrawRange(ref area, Labels, _Objects, firstVisibleField, Mathf.Min(lastVisibleField, _Suggestions)); area.height = InternalGUI.HeaddingStyle.fixedHeight; GUI.Label(area, "Other Options", InternalGUI.HeaddingStyle); area.y = area.yMax; DrawRange(ref area, Labels, _Objects, Mathf.Max(_Suggestions, firstVisibleField), lastVisibleField); } } /************************************************************************************************************************/ private void DrawSearchedOptions(Rect area, int firstVisibleField, int lastVisibleField) { area.y = firstVisibleField * InternalGUI.LabelHeight; DrawRange(ref area, SearchedLabels, SearchedObjects, firstVisibleField, lastVisibleField); } /************************************************************************************************************************/ private void DrawRange(ref Rect area, List labels, IList objects, int start, int end) { area.height = InternalGUI.LabelHeight; if (end > labels.Count) end = labels.Count; for (; start < end; start++) { DrawOption(area, labels, objects, start); area.y = area.yMax; } } /************************************************************************************************************************/ private void DrawOption(Rect area, List labels, IList objects, int index) { var obj = objects[index]; var wasOn = obj == _SelectedObject; var isOn = GUI.Toggle(area, wasOn, labels[index], wasOn ? InternalGUI.SelectedButtonStyle : InternalGUI.ButtonStyle); if (isOn != wasOn) { if (wasOn) { PickAndClose(); } else if (isOn) { _SelectedObject = obj; } } } /************************************************************************************************************************/ private void Update() { if (focusedWindow != this) Close(); } /************************************************************************************************************************/ private void OffsetSelectedIndex(int offset) { var objects = HasSearchText ? SearchedObjects : _Objects; if (objects.Count == 0) return; var index = objects.IndexOf(_SelectedObject); if (index >= 0) _SelectedObject = objects[Mathf.Clamp(index + offset, 0, objects.Count)]; else _SelectedObject = objects[0]; } /************************************************************************************************************************/ private static class InternalGUI { public static readonly GUIStyle SearchBarStyle, SearchBarEndStyle, SearchBarCancelStyle, HeaddingStyle, ButtonStyle, SelectedButtonStyle; public static float SearchBarHeight { get { return EditorStyles.toolbar.fixedHeight; } } public static float LabelHeight { get { return ButtonStyle.fixedHeight; } } static InternalGUI() { SearchBarStyle = GUI.skin.FindStyle("ToolbarSeachTextField"); SearchBarEndStyle = GUI.skin.FindStyle("ToolbarSeachCancelButtonEmpty"); SearchBarCancelStyle = GUI.skin.FindStyle("ToolbarSeachCancelButton"); HeaddingStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 12, alignment = TextAnchor.MiddleLeft, fixedHeight = 22 }; ButtonStyle = new GUIStyle(EditorStyles.toolbarButton) { alignment = TextAnchor.MiddleLeft, fontSize = 12 }; SelectedButtonStyle = new GUIStyle(ButtonStyle) { fontStyle = FontStyle.Bold }; } } /************************************************************************************************************************/ } } #endif