namespace Zinnia.Utility
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
///
/// An editor window that shows a selection of elements that can be chosen as a desired value.
///
/// The element type to show.
/// The
public class PickerWindow : EditorWindow where TSelf : PickerWindow
{
///
/// State key text to search the session state for.
///
protected static readonly string SearchTextSessionStateKey = typeof(PickerWindow<,>).FullName + nameof(SearchTextSessionStateKey);
///
/// A collection of elements to pick from.
///
protected readonly List elements = new List();
///
/// A collection of results from an executed search.
///
protected readonly List searchResults = new List();
///
/// The selected element action.
///
protected Action elementSelector;
///
/// The function to perform when extracting the search term.
///
protected Func searchTermExtractor;
///
/// The function for drawing the element drawer.
///
protected Func elementDrawer;
///
/// The component for searching the elements.
///
protected SearchField searchField;
///
/// The style for the search label.
///
protected GUIStyle searchLabelPrefixStyle;
///
/// The content for the search label.
///
protected GUIContent searchLabelPrefixContent;
///
/// The style for the element.
///
protected GUIStyle elementStyle;
///
/// The height of the element.
///
protected float elementHeight;
///
/// The text that is being searched for.
///
protected string searchText = string.Empty;
///
/// The current component scroll position.
///
protected Vector2 scrollPosition = Vector2.zero;
///
/// The count of how many results are visible.
///
protected int? visibleCount;
///
/// Whether to scroll to the currently selected index.
///
protected bool scrollToSelectedIndex;
///
/// The current selected index.
///
protected int selectedIndex;
public static TSelf Show(
Rect sourceRect,
IEnumerable elements,
Action elementSelector,
Func searchTermExtractor,
Func elementDrawer)
{
TSelf window = CreateInstance();
window.elementSelector = elementSelector;
window.searchTermExtractor = searchTermExtractor;
window.elementDrawer = elementDrawer;
window.elements.AddRange(elements);
window.FindElements(SessionState.GetString(SearchTextSessionStateKey, string.Empty));
window.ShowAsDropDown(sourceRect, new Vector2(Mathf.Max(250f, sourceRect.width), 350f));
return window;
}
protected virtual void Awake()
{
titleContent = new GUIContent("Picker");
wantsMouseMove = true;
searchField = new SearchField();
searchLabelPrefixStyle = new GUIStyle(EditorStyles.label)
{
richText = true
};
string color = EditorGUIUtility.isProSkin ? "4F80F8" : "0808FC";
searchLabelPrefixContent = new GUIContent(
$"Search (Regex):",
"This search supports regular expressions. Click to learn more about them.");
elementStyle = new GUIStyle(EditorStyles.label);
elementHeight = elementStyle.CalcSize(GUIContent.none).y;
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
}
protected virtual void OnBeforeAssemblyReload()
{
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
try
{
Close();
}
catch
{
DestroyImmediate(this);
}
}
protected virtual void OnGUI()
{
// The search field drawn below will use keyboard events, thus we need to check them before it's drawn.
HandleKeyboard();
using (new EditorGUILayout.VerticalScope("grey_border"))
{
DrawSearch();
DrawList();
}
}
///
/// Handles the keyboard events.
///
protected virtual void HandleKeyboard()
{
Event currentEvent = Event.current;
if (currentEvent.type != EventType.KeyDown)
{
return;
}
switch (currentEvent.keyCode)
{
case KeyCode.Escape when string.IsNullOrEmpty(searchText):
Close();
GUIUtility.ExitGUI();
currentEvent.Use();
break;
case KeyCode.DownArrow:
selectedIndex++;
scrollToSelectedIndex = true;
currentEvent.Use();
break;
case KeyCode.UpArrow:
selectedIndex--;
scrollToSelectedIndex = true;
currentEvent.Use();
break;
case KeyCode.Return:
case KeyCode.KeypadEnter:
elementSelector(searchResults[selectedIndex]);
Close();
GUIUtility.ExitGUI();
currentEvent.Use();
break;
}
}
///
/// Draws the search component.
///
protected virtual void DrawSearch()
{
GUILayout.Space(5f);
using (new EditorGUILayout.HorizontalScope())
{
GUILayout.Space(5f);
Vector2 labelSize = searchLabelPrefixStyle.CalcSize(searchLabelPrefixContent);
GUILayoutOption widthLayoutOption = GUILayout.Width(labelSize.x);
using (new EditorGUILayout.VerticalScope(widthLayoutOption))
{
GUILayout.Space(3f);
EditorGUILayout.LabelField(searchLabelPrefixContent, searchLabelPrefixStyle, widthLayoutOption);
Rect labelRect = GUILayoutUtility.GetLastRect();
EditorGUIUtility.AddCursorRect(labelRect, MouseCursor.Link);
Event currentEvent = Event.current;
if (currentEvent.type == EventType.MouseUp && labelRect.Contains(currentEvent.mousePosition))
{
Application.OpenURL("https://docs.microsoft.com/en-us/dotnet/standard/base-types/regular-expressions");
currentEvent.Use();
}
}
searchField.SetFocus();
string newSearchText = searchField.OnGUI(EditorGUILayout.GetControlRect(false), searchText);
if (searchText != newSearchText)
{
FindElements(newSearchText);
}
GUILayout.Space(3f);
}
}
///
/// Draws the list component.
///
protected virtual void DrawList()
{
Event currentEvent = Event.current;
Rect? selectedRect;
using (EditorGUILayout.ScrollViewScope scrollViewScope = new EditorGUILayout.ScrollViewScope(scrollPosition, EditorStyles.helpBox))
{
scrollPosition = scrollViewScope.scrollPosition;
selectedIndex = Mathf.Clamp(selectedIndex, 0, searchResults.Count - 1);
int startIndex = visibleCount == null
? 0
: Mathf.Clamp((int)(scrollPosition.y / elementHeight), 0, searchResults.Count - visibleCount.Value);
int count = visibleCount == null
? searchResults.Count
: Mathf.Min(startIndex + visibleCount.Value, searchResults.Count);
GUILayout.Space(startIndex * elementHeight);
selectedRect = DrawElements(startIndex, count);
GUILayout.Space(Mathf.Max(0, (searchResults.Count - startIndex - (visibleCount ?? searchResults.Count)) * elementHeight));
}
Rect scrollViewRect = GUILayoutUtility.GetLastRect();
if (scrollToSelectedIndex && currentEvent.type == EventType.Repaint)
{
if (selectedRect == null)
{
selectedRect = new Rect(0f, selectedIndex * elementHeight, 0f, 0f);
}
scrollToSelectedIndex = false;
Rect selectedRectValue = selectedRect.Value;
if (selectedRectValue.yMax - scrollViewRect.height > scrollPosition.y)
{
scrollPosition.y = selectedRectValue.yMax - scrollViewRect.height;
Repaint();
}
else if (selectedRectValue.y < scrollPosition.y)
{
scrollPosition.y = selectedRectValue.y;
Repaint();
}
}
if (currentEvent.type == EventType.Repaint)
{
visibleCount = Mathf.Clamp(Mathf.CeilToInt(scrollViewRect.height / elementHeight), 0, searchResults.Count);
}
}
///
/// Draws the elements.
///
/// The index to display the results from.
/// The total number of results to display.
/// The position in where to draw the component.
protected virtual Rect? DrawElements(int startIndex, int count)
{
Color previousBackgroundColor = GUI.backgroundColor;
Texture2D previousNormalBackground = elementStyle.normal.background;
Event currentEvent = Event.current;
Rect? selectedRect = null;
for (int index = startIndex; index < count; index++)
{
bool isSelected = selectedIndex == index;
if (isSelected)
{
GUI.backgroundColor = GUI.skin.settings.selectionColor;
elementStyle.normal.background = Texture2D.whiteTexture;
}
TElement element = searchResults[index];
EditorGUILayout.LabelField(elementDrawer(element), elementStyle);
Rect rect = GUILayoutUtility.GetLastRect();
if (isSelected)
{
selectedRect = rect;
elementStyle.normal.background = previousNormalBackground;
GUI.backgroundColor = previousBackgroundColor;
}
bool isHoveredOver = rect.Contains(currentEvent.mousePosition);
if ((currentEvent.type == EventType.MouseMove || currentEvent.type == EventType.MouseDrag)
&& !isSelected
&& isHoveredOver)
{
selectedIndex = index;
currentEvent.Use();
}
if (currentEvent.type != EventType.MouseUp || !isHoveredOver)
{
continue;
}
selectedIndex = index;
elementSelector(searchResults[selectedIndex]);
Close();
GUIUtility.ExitGUI();
currentEvent.Use();
}
return selectedRect;
}
///
/// Finds the elements that match the given string.
///
/// The text to search for.
protected virtual void FindElements(string searchText)
{
searchText = searchText ?? string.Empty;
this.searchText = searchText;
SessionState.SetString(SearchTextSessionStateKey, searchText);
const RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.Compiled;
Regex regex;
try
{
regex = new Regex(this.searchText, options);
}
catch
{
regex = new Regex(".*", options);
}
searchResults.Clear();
searchResults.AddRange(elements.Where(element => regex.IsMatch(searchTermExtractor(element))));
if (visibleCount != null)
{
visibleCount = Mathf.Clamp(visibleCount.Value, 0, searchResults.Count);
}
}
}
}