You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
CrowdControl/Assets/3rd/Plugins/UltEvents/Inspector/ObjectPicker.cs

722 lines
28 KiB
C#

3 months ago
// 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
{
/// <summary>[Editor-Only] Allows you to draw GUI fields which can be used to pick an object from a list.</summary>
public static class ObjectPicker
{
/************************************************************************************************************************/
#region Main Drawing Methods
/************************************************************************************************************************/
/// <summary>Draws a field which lets you pick an object from a list and returns the selected object.</summary>
public static T Draw<T>(Rect area, T selected, Func<List<T>> getOptions, int suggestions, Func<T, GUIContent> getLabel, Func<T> 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;
}
/// <summary>Draws a field which lets you pick an object from a list and returns the selected object.</summary>
public static T Draw<T>(Rect area, T selected, Func<List<T>> getOptions, int suggestions, Func<T, GUIContent> getLabel, Func<T> getDragAndDrop)
{
return Draw(area, selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle);
}
/************************************************************************************************************************/
/// <summary>Draws a field (using GUILayout) which lets you pick an object from a list and returns the selected object.</summary>
public static T DrawLayout<T>(T selected, Func<List<T>> getOptions, int suggestions, Func<T, GUIContent> getLabel, Func<T> 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;
}
/// <summary>Draws a field (using GUILayout) which lets you pick an object from a list and returns the selected object.</summary>
public static T DrawLayout<T>(T selected, Func<List<T>> getOptions, int suggestions, Func<T, GUIContent> getLabel, Func<T> getDragAndDrop,
params GUILayoutOption[] layoutOptions)
{
return DrawLayout(selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle, layoutOptions);
}
/************************************************************************************************************************/
/// <summary>
/// Draws a field (as an inspector field using GUILayout) which lets you pick an object from a list and returns
/// the selected object.
/// </summary>
public static T DrawEditorLayout<T>(GUIContent label, T selected, Func<List<T>> getOptions, int suggestions,
Func<T, GUIContent> getLabel, Func<T> 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;
}
/// <summary>
/// Draws a field (as an inspector field using GUILayout) which lets you pick an object from a list and returns
/// the selected object.
/// </summary>
public static T DrawEditorLayout<T>(GUIContent label, T selected, Func<List<T>> getOptions, int suggestions,
Func<T, GUIContent> getLabel, Func<T> getDragAndDrop, params GUILayoutOption[] options)
{
return DrawEditorLayout(label, selected, getOptions, suggestions, getLabel, getDragAndDrop, InternalGUI.TypeButtonStyle, options);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Type Field
/************************************************************************************************************************/
/// <summary>Draws a field which lets you pick a <see cref="Type"></see> from a list and returns the selected type.</summary>
public static Type DrawTypeField(Rect area, Type selected, Func<List<Type>> 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);
}
/************************************************************************************************************************/
/// <summary>Draws a field which lets you pick an asset <see cref="Type"></see> from a list and returns the selected type.</summary>
public static Type DrawAssetTypeField(Rect area, Type selected, Func<List<Type>> 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);
}
/************************************************************************************************************************/
/// <summary>Draws a field which lets you pick a <see cref="Type"></see> from a list and returns the selected <see cref="Type.AssemblyQualifiedName"/>.</summary>
public static string DrawTypeField(Rect area, string selectedTypeName, Func<List<Type>> 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
/************************************************************************************************************************/
/// <summary>
/// Removes any duplicates of the first few elements in `options` (from 0 to `suggestions`) from anywhere later
/// in the list.
/// </summary>
public static void RemoveDuplicateSuggestions<T>(List<T> 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<T>(ref T selected)
{
var id = GUIUtility.GetControlID(FocusType.Passive);
ObjectPickerWindow.TryGetPickedObject(id, ref selected);
return id;
}
/************************************************************************************************************************/
private static void CheckDragAndDrop<T>(Rect area, ref T selected, Func<List<T>> getOptions, Func<T> 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<GUIContent>
Labels = new List<GUIContent>(),
SearchedLabels = new List<GUIContent>();
private readonly List<object>
SearchedObjects = new List<object>();
[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<T>(int fieldID, T selected, List<T> objects, int suggestions, Func<T, GUIContent> getLabel)
{
if (objects == null || objects.Count == 0)
{
Debug.LogError("'objects' list is null or empty.");
return;
}
_FieldID = fieldID;
_HasPickedObject = false;
var window = CreateInstance<ObjectPickerWindow>();
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<T>(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<GUIContent> 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<GUIContent> 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