using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using UnityEditor;
using UnityEngine;
namespace Unity.VisualScripting
{
public sealed class FuzzyWindow : EditorWindow
{
private static readonly Color lightSkinColor = new Color(0.75f, 0.75f, 0.75f);
private static Color backgroundWindowColor;
public void Populate(FuzzyOptionNode node, IEnumerable<object> childrenValues, CancellationToken? cancellation = null)
{
if (node.isPopulated)
{
return;
}
var i = 0;
var _childrenValues = childrenValues.ToArray();
lock (guiLock)
{
node.hasChildren = _childrenValues.Length > 0;
}
foreach (var childValue in _childrenValues)
{
var childOption = tree.Option(childValue);
try
{
childOption.OnPopulate();
}
catch (ThreadAbortException)
{
throw;
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to display {childOption.GetType()}: \n{ex}");
continue;
}
DisplayProgressBar($"{childOption.label}... ({++i} / {_childrenValues.Length})", (float)i / _childrenValues.Length);
var hasChildren = tree.Children(childValue).Any();
var include = !childOption.parentOnly || hasChildren;
if (!include)
{
continue;
}
string label;
if (node == searchRoot)
{
label = tree.SearchResultLabel(childValue, query);
}
else if (node == favoritesRoot)
{
label = tree.FavoritesLabel(childValue);
}
else
{
label = childOption.label;
}
var childNode = new FuzzyOptionNode(childOption, label);
childNode.hasChildren = hasChildren;
lock (guiLock)
{
node.children.Add(childNode);
}
cancellation?.ThrowIfCancellationRequested();
}
lock (guiLock)
{
node.isPopulated = true;
}
}
static FuzzyWindow()
{
backgroundWindowColor = EditorGUIUtility.isProSkin ? GUI.color : lightSkinColor;
ShowAsDropDownFitToScreen = typeof(FuzzyWindow).GetMethod("ShowAsDropDownFitToScreen", BindingFlags.Instance | BindingFlags.NonPublic);
}
public static GUIStyle defaultOptionStyle => Styles.optionWithIcon;
private static Event e => Event.current;
public static class Styles
{
static Styles()
{
header = new GUIStyle("In BigTitle");
header.font = EditorStyles.boldLabel.font;
header.normal.textColor = ColorPalette.unityForeground;
header.alignment = TextAnchor.MiddleCenter;
header.padding = new RectOffset(0, 0, 0, 0);
footerBackground = new GUIStyle("In BigTitle");
optionWithIcon = new GUIStyle("PR Label");
optionWithIcon.richText = true;
optionWithIcon.alignment = TextAnchor.MiddleLeft;
optionWithIcon.padding.left -= 15;
optionWithIcon.fixedHeight = 20f;
optionWithoutIcon = new GUIStyle(optionWithIcon);
optionWithoutIcon.padding.left += 17;
optionWithIconDim = new GUIStyle(optionWithIcon);
optionWithIconDim.normal.textColor = ColorPalette.unityForegroundDim;
optionWithoutIconDim = new GUIStyle(optionWithIcon);
optionWithoutIconDim.normal.textColor = ColorPalette.unityForegroundDim;
background = new GUIStyle("grey_border");
rightArrow = new GUIStyle("AC RightArrow");
leftArrow = new GUIStyle("AC LeftArrow");
searchField = new GUIStyle("SearchTextField");
searchFieldCancelButton = new GUIStyle("SearchCancelButton");
searchFieldCancelButtonEmpty = new GUIStyle("SearchCancelButtonEmpty");
insufficientSearch = new GUIStyle(EditorStyles.centeredGreyMiniLabel);
insufficientSearch.normal.textColor = Color.grey;
insufficientSearch.wordWrap = true;
insufficientSearch.padding = new RectOffset(10, 10, 10, 10);
insufficientSearch.alignment = TextAnchor.MiddleCenter;
check = new GUIStyle();
var checkTexture = BoltCore.Resources.LoadTexture("Check.png", new TextureResolution[] { 12, 24 }, CreateTextureOptions.PixelPerfect);
check.normal.background = checkTexture?[12];
check.normal.scaledBackgrounds = new[] { checkTexture?[24] };
check.fixedHeight = 12;
check.fixedWidth = 12;
star = new GUIStyle();
var starOffTexture = BoltCore.Resources.LoadIcon("StarOff.png");
var starOnTexture = BoltCore.Resources.LoadIcon("StarOn.png");
star.normal.background = starOffTexture?[16];
star.normal.scaledBackgrounds = new[] { starOffTexture?[32] };
star.onNormal.background = starOnTexture?[16];
star.onNormal.scaledBackgrounds = new[] { starOnTexture?[32] };
star.fixedHeight = 16;
star.fixedWidth = 16;
favoritesIcon = starOnTexture;
}
public static readonly GUIStyle header;
public static readonly GUIStyle footerBackground;
public static readonly GUIStyle optionWithIcon;
public static readonly GUIStyle optionWithoutIcon;
public static readonly GUIStyle optionWithIconDim;
public static readonly GUIStyle optionWithoutIconDim;
public static readonly GUIStyle background;
public static readonly GUIStyle rightArrow;
public static readonly GUIStyle leftArrow;
public static readonly GUIStyle searchField;
public static readonly GUIStyle searchFieldCancelButton;
public static readonly GUIStyle searchFieldCancelButtonEmpty;
public static readonly GUIStyle insufficientSearch;
public static readonly GUIStyle check;
public static readonly GUIStyle star;
public static readonly EditorTexture favoritesIcon;
public static readonly float searchFieldHeight = 20;
public static readonly float headerHeight = 25;
public static readonly float optionHeight = 20;
public static readonly float maxOptionWidth = 800;
}
private class Root : FuzzyOption<object>
{
public Root(GUIContent header)
{
label = header.text;
icon = EditorTexture.Single(header.image);
}
}
private class SearchRoot : FuzzyOption<object>
{
public SearchRoot()
{
label = searchHeader;
}
}
public class FavoritesRoot : FuzzyOption<object>
{
public FavoritesRoot()
{
label = "Favorites";
icon = Styles.favoritesIcon;
}
}
#region Lifecycle
private Action<IFuzzyOption> callback;
[DoNotSerialize]
public static FuzzyWindow instance { get; private set; }
private IFuzzyOptionTree tree;
private bool requireRepaint;
public static void Show(Rect activatorPosition, IFuzzyOptionTree optionTree, Action<IFuzzyOption> callback)
{
Ensure.That(nameof(optionTree)).IsNotNull(optionTree);
// Makes sure control exits DelayedTextFields before opening the window
GUIUtility.keyboardControl = 0;
if (instance != null)
{
instance.Close();
}
else
{
instance = CreateInstance<FuzzyWindow>();
instance.Initialize(optionTree, callback);
instance.CreateWindow(activatorPosition);
}
}
private void OnEnable()
{
query = string.Empty;
}
private void OnDisable()
{
instance = null;
}
#if UNITY_EDITOR_LINUX
private void OnLostFocus()
{
Close();
}
#endif
private void Update()
{
//This variable should not be null, but when we click the play button the FuzzyFinder (DropDown window) does not close on 2019 and 2020
//but it does close on 2018.
//The FuzzyFinder (DropDown window) should close if you click anywhere in the Editor
//Once this issue is fixed this null check can be removed
if (stack != null)
{
activeParent.isLoading = !activeParent.isPopulated;
if (requireRepaint)
{
Repaint();
requireRepaint = false;
}
}
}
private void CreateWindow(Rect activatorPosition)
{
// Port the activator position to screen space
activatorPosition.position = GUIUtility.GUIToScreenPoint(activatorPosition.position);
this.activatorPosition = activatorPosition;
// Show and focus the window
wantsMouseMove = true;
var initialSize = new Vector2(activatorPosition.width, height);
this.ShowAsDropDownWithKeyboardFocus(activatorPosition, initialSize);
Focus();
}
private void Initialize(IFuzzyOptionTree optionTree, Action<IFuzzyOption> callback)
{
tree = optionTree;
// Create the hierarchy
stack = new Stack<FuzzyOptionNode>();
root = new FuzzyOptionNode(new Root(optionTree.header));
ExecuteTask(delegate
{
optionTree.Prewarm();
Populate(root, optionTree.Root());
// Fit height to children if there is no depth and no search
var hasSubChildren = root.children.Any(option => option.hasChildren);
if (!optionTree.searchable && !hasSubChildren)
{
height = 0;
if (!string.IsNullOrEmpty(root.option.headerLabel))
{
height += Styles.headerHeight;
}
height += root.children.Count * Styles.optionHeight + 1;
}
});
// Add favorites
favoritesRoot = new FuzzyOptionNode(new FavoritesRoot());
UpdateFavorites();
// Setup the search
searchRoot = new FuzzyOptionNode(new SearchRoot());
Search();
// Assign the callback
this.callback = callback;
// Show and focus the window
}
#endregion
#region Hierarchy
private FuzzyOptionNode root;
private FuzzyOptionNode searchRoot;
private Stack<FuzzyOptionNode> stack;
private FuzzyOptionNode activeRoot => !hasSearch ? root : searchRoot;
private FuzzyOptionNode activeParent => stack.Peek();
private int activeSelectedIndex
{
get
{
return activeParent.selectedIndex;
}
set
{
lock (guiLock)
{
activeParent.selectedIndex = value;
}
}
}
private IList<FuzzyOptionNode> activeNodes => activeParent.children;
private FuzzyOptionNode activeNode
{
get
{
if (activeSelectedIndex >= 0 && activeSelectedIndex < activeNodes.Count)
{
return activeNodes[activeSelectedIndex];
}
else
{
return null;
}
}
}
private FuzzyOptionNode GetLevelRelative(int levelOffset)
{
return stack.Skip(-levelOffset).FirstOrDefault();
}
private void SelectParent()
{
if (stack.Count > 1)
{
animTarget = 0;
lastRepaintTime = DateTime.Now;
}
}
private void EnterChild(FuzzyOptionNode node)
{
if (node == null || !node.hasChildren)
{
return;
}
ExecuteTask(delegate
{
Populate(node, tree.Children(node.option.value));
});
lastRepaintTime = DateTime.Now;
if (animTarget == 0)
{
animTarget = 1;
}
else if (anim == 1)
{
anim = 0;
stack.Push(node);
}
}
private void SelectChild(FuzzyOptionNode node)
{
if (node == null)
{
return;
}
if (node.hasChildren)
{
EnterChild(node);
}
else if (callback != null)
{
callback(node.option);
}
}
#endregion
#region Search
private int minSearchLength = 2;
private string query;
private string delayedQuery;
private bool letQueryClear;
private string searchFieldName = "FuzzySearch";
private static string searchHeader = "Search";
private CancellationTokenSource searchCancellationTokenSource;
private bool hasSearch => !string.IsNullOrEmpty(query);
private bool hasSufficientSearch => hasSearch && query.Length >= minSearchLength;
private void Search()
{
stack.Clear();
if (hasSearch)
{
searchCancellationTokenSource?.Cancel();
if (hasSufficientSearch)
{
var queryNow = query;
searchCancellationTokenSource = new CancellationTokenSource();
var searchCancellationToken = searchCancellationTokenSource.Token;
ExecuteTask(delegate
{
if (searchCancellationToken.IsCancellationRequested)
{
return;
}
DisplayProgressBar($"Searching for '{queryNow}'...", 0);
lock (guiLock)
{
searchRoot.children.Clear();
searchRoot.isPopulated = false;
}
Populate(searchRoot, tree.OrderedSearchResults(query, searchCancellationToken).Cancellable(searchCancellationToken).Take(BoltCore.Configuration.maxSearchResults));
activeSelectedIndex = activeNodes.Count >= 1 ? 0 : -1;
});
}
else
{
ExecuteTask(delegate
{
lock (guiLock)
{
searchRoot.children.Clear();
searchRoot.isPopulated = true;
}
});
}
stack.Push(searchRoot);
}
else
{
stack.Push(root);
animTarget = 1;
lastRepaintTime = DateTime.Now;
return;
}
}
#endregion
#region Favorites
private FuzzyOptionNode favoritesRoot;
private void UpdateFavorites()
{
ExecuteTask(delegate
{
if (tree.favorites != null)
{
DisplayProgressBar("Fetching favorites...", 0);
favoritesRoot.children.Clear();
favoritesRoot.isPopulated = false;
// Adding a where clause in case a favorited item was later changed to be unfavoritable.
Populate(favoritesRoot, tree.favorites.Where(favorite => tree.CanFavorite(favorite)));
}
else
{
favoritesRoot.children.Clear();
favoritesRoot.isPopulated = true;
}
root.children.Remove(favoritesRoot);
if (favoritesRoot.hasChildren)
{
root.children.Insert(0, favoritesRoot);
}
});
}
#endregion
#region Animation
private float anim = 1;
private int animTarget = 1;
private float animationSpeed = 4;
private DateTime lastRepaintTime;
public float repaintDeltaTime => (float)(DateTime.Now - lastRepaintTime).TotalSeconds;
private bool isAnimating => anim != animTarget;
#endregion
#region Positioning
private static readonly MethodInfo ShowAsDropDownFitToScreen;
private float maxHeight = 320;
private float height = 320;
private float minWidth = 200;
private float minOptionWidth;
private float headerWidth;
private float footerHeight;
private Rect activatorPosition;
private bool scrollToSelected;
private float initialY;
private bool initialYSet;
private void OnPositioning()
{
if (Event.current.type != EventType.Repaint)
{
return;
}
if (!initialYSet)
{
initialY = this.position.y;
initialYSet = true;
}
var totalWidth = Mathf.Max(minWidth, activatorPosition.width, minOptionWidth + 36, headerWidth + 36);
var totalHeight = Mathf.Min(height, maxHeight);
var position = (Rect)ShowAsDropDownFitToScreen.Invoke(this, new object[] { activatorPosition, new Vector2(totalWidth, totalHeight), null });
position.y = initialY;
if (!isAnimating && activeNode?.option != null && activeNode.option.hasFooter)
{
var newfooterHeight = activeNode.option.GetFooterHeight(totalWidth - footerWidthMargin);
footerHeight = footerHeight < newfooterHeight ? newfooterHeight : footerHeight;
}
position.height += footerHeight;
if (this.position != position || minSize != position.size)
{
minSize = maxSize = position.size;
this.position = position;
}
GUIUtility.ExitGUI();
}
#endregion
#region GUI
//Be sure do not call any layout change while the UI is repainting or force a repaint
//before the layout is completed.
//This will probably break the UI
private void OnGUI()
{
try
{
lock (guiLock)
{
//This variable should not be null, but when we click the play button the FuzzyFinder (DropDown window) does not close on 2019 and 2020
//but it does close on 2018.
//The FuzzyFinder (DropDown window) should close if you click anywhere in the Editor
//Once this issue is fixed this null check can be removed
if (tree != null)
{
GUI.Label(new Rect(0, 0, position.width, position.height), GUIContent.none, Styles.background);
HandleKeyboard();
if (tree.searchable)
{
LudiqGUI.Space(7);
if (letQueryClear)
{
letQueryClear = false;
}
else
{
EditorGUI.FocusTextInControl(searchFieldName);
}
var searchFieldPosition = GUILayoutUtility.GetRect(10, Styles.searchFieldHeight);
searchFieldPosition.x += 8;
searchFieldPosition.width -= 16;
var newQuery = OnSearchGUI(searchFieldPosition, delayedQuery ?? query);
if (newQuery != query || delayedQuery != null)
{
if (!isAnimating)
{
query = delayedQuery ?? newQuery;
Search();
delayedQuery = null;
}
else
{
delayedQuery = newQuery;
}
}
}
OnLevelGUI(anim, GetLevelRelative(0), GetLevelRelative(-1));
if (anim < 1)
{
OnLevelGUI(anim + 1, GetLevelRelative(-1), GetLevelRelative(-2));
}
if (isAnimating && e.type == EventType.Repaint)
{
anim = Mathf.MoveTowards(anim, animTarget, repaintDeltaTime * animationSpeed);
if (animTarget == 0 && anim == 0)
{
anim = 1;
animTarget = 1;
stack.Pop();
}
requireRepaint = true;
}
if (e.type == EventType.Repaint)
{
lastRepaintTime = DateTime.Now;
}
if (!activeParent.isLoading)
{
if (tree.searchable && hasSearch && !hasSufficientSearch)
{
EditorGUI.LabelField
(
new Rect(0, 0, position.width, position.height),
$"Enter at least {minSearchLength} characters to search.",
Styles.insufficientSearch
);
}
OnFooterGUI();
OnPositioning();
}
}
}
}
catch (ArgumentException ex)
{
if (tree.multithreaded && ex.Message.StartsWith("Getting control "))
{
// A bunch of happens that might affect the GUI could happen on a
// secondary thread, leading to Unity complaining about the amount
// of controls changing between the draw call and the layout call.
// Because these are hamless and last just one frame, we can safely
// ignore them and repaint right away.
requireRepaint = true;
}
else
{
throw;
}
}
}
private string OnSearchGUI(Rect position, string query)
{
var fieldPosition = position;
fieldPosition.width -= 15;
var cancelButtonPosition = position;
cancelButtonPosition.x += position.width - 15;
cancelButtonPosition.width = 15;
GUI.SetNextControlName(searchFieldName);
query = EditorGUI.TextField(fieldPosition, query, Styles.searchField);
if (GUI.Button(cancelButtonPosition, GUIContent.none, string.IsNullOrEmpty(query) ? Styles.searchFieldCancelButtonEmpty : Styles.searchFieldCancelButton) && query != string.Empty)
{
query = string.Empty;
GUIUtility.keyboardControl = 0;
letQueryClear = true;
}
return query;
}
private void OnHeaderGUI(FuzzyOptionNode parent, Rect headerPosition)
{
EditorGUIUtility.SetIconSize(new Vector2(IconSize.Small, IconSize.Small));
var headerContent = new GUIContent(parent.option.headerLabel, parent.option.showHeaderIcon ? parent.option.icon?[IconSize.Small] : null);
headerWidth = Styles.header.CalcSize(headerContent).x;
if (e.type == EventType.Repaint)
{
GUI.Label(headerPosition, headerContent, Styles.header);
}
EditorGUIUtility.SetIconSize(default(Vector2));
}
private void OnLevelGUI(float anim, FuzzyOptionNode parent, FuzzyOptionNode grandParent)
{
anim = Mathf.Floor(anim) + Mathf.SmoothStep(0, 1, Mathf.Repeat(anim, 1));
var levelPosition = new Rect
(
position.width * (1 - anim) + 1,
tree.searchable ? 30 : 0,
position.width - 2,
height - (tree.searchable ? 31 : 1)
);
GUILayout.BeginArea(levelPosition);
if (grandParent != null || !string.IsNullOrEmpty(parent.option.headerLabel))
{
var headerPosition = GUILayoutUtility.GetRect(10, Styles.headerHeight);
OnHeaderGUI(parent, headerPosition);
if (grandParent != null)
{
var leftArrowPosition = new Rect(headerPosition.x + 4, headerPosition.y + 7, 13, 13);
if (e.type == EventType.Repaint)
{
Styles.leftArrow.Draw(leftArrowPosition, false, false, false, false);
}
if (!isAnimating)
{
if (e.type == EventType.MouseDown && (e.button == (int)MouseButton.Right || headerPosition.Contains(e.mousePosition)))
{
SelectParent();
e.Use();
GUIUtility.ExitGUI();
}
}
}
}
OnOptionsGUI(parent, levelPosition.height - Styles.headerHeight);
GUILayout.EndArea();
}
private Vector2 lastMouseMovePosition;
private void OnOptionsGUI(FuzzyOptionNode parent, float scrollViewHeight)
{
if (parent.isLoading || (tree.showBackgroundWorkerProgress && BackgroundWorker.hasProgress))
{
LudiqGUI.BeginVertical();
LudiqGUI.FlexibleSpace();
LudiqGUI.BeginHorizontal();
LudiqGUI.FlexibleSpace();
LudiqGUI.LoaderLayout();
LudiqGUI.FlexibleSpace();
LudiqGUI.EndHorizontal();
LudiqGUI.Space(16);
LudiqGUI.BeginHorizontal();
LudiqGUI.Space(10);
var progressBarPosition = GUILayoutUtility.GetRect(GUIContent.none, GUIStyle.none, GUILayout.Height(19), GUILayout.ExpandWidth(true));
if (progressBarPosition.width > 0 && progress > 0)
{
if (tree.showBackgroundWorkerProgress && BackgroundWorker.hasProgress)
{
EditorGUI.ProgressBar(progressBarPosition, BackgroundWorker.progressProportion, BackgroundWorker.progressLabel);
}
else if (showProgress)
{
EditorGUI.ProgressBar(progressBarPosition, progress, progressText);
}
}
LudiqGUI.Space(10);
LudiqGUI.EndHorizontal();
LudiqGUI.FlexibleSpace();
LudiqGUI.EndVertical();
return;
}
parent.scroll = GUILayout.BeginScrollView(parent.scroll);
EditorGUIUtility.SetIconSize(new Vector2(IconSize.Small, IconSize.Small));
var selectedOptionPosition = default(Rect);
if (e.type == EventType.Repaint)
{
minOptionWidth = 0;
}
foreach (var node in parent.children)
{
node.EnsureDrawable();
minOptionWidth = Mathf.Max(minOptionWidth, Mathf.Min(node.width, Styles.maxOptionWidth));
}
for (var i = 0; i < parent.children.Count; i++)
{
var node = parent.children[i];
var optionPosition = GUILayoutUtility.GetRect(IconSize.Small, Styles.optionHeight, GUILayout.ExpandWidth(true));
if (!isAnimating)
{
if (((e.type == EventType.MouseMove && GUIUtility.GUIToScreenPoint(e.mousePosition) != lastMouseMovePosition) || e.type == EventType.MouseDown) &&
parent.selectedIndex != i && optionPosition.Contains(e.mousePosition))
{
parent.selectedIndex = i;
requireRepaint = true;
lastMouseMovePosition = GUIUtility.GUIToScreenPoint(e.mousePosition);
GUIUtility.ExitGUI();
}
}
var optionIsSelected = false;
if (i == parent.selectedIndex)
{
optionIsSelected = true;
selectedOptionPosition = optionPosition;
}
// Clipping
if (optionPosition.yMax < parent.scroll.y || optionPosition.yMin > parent.scroll.y + scrollViewHeight)
{
continue;
}
if (e.type == EventType.Repaint)
{
node.style.Draw(optionPosition, node.label, false, false, optionIsSelected, optionIsSelected);
}
var right = optionPosition.xMax;
if (node.hasChildren)
{
right -= 13;
var rightArrowPosition = new Rect(right, optionPosition.y + 4, 13, 13);
if (e.type == EventType.Repaint)
{
Styles.rightArrow.Draw(rightArrowPosition, false, false, false, false);
}
}
if (!node.hasChildren && tree.selected.Contains(node.option.value))
{
right -= 16;
var checkPosition = new Rect(right, optionPosition.y + 4, 12, 12);
if (e.type == EventType.Repaint)
{
Styles.check.Draw(checkPosition, false, false, false, false);
}
}
if (tree.favorites != null && tree.CanFavorite(node.option.value) && (optionIsSelected || tree.favorites.Contains(node.option.value)))
{
right -= 19;
var starPosition = new Rect(right, optionPosition.y + 2, IconSize.Small, IconSize.Small);
EditorGUI.BeginChangeCheck();
var isFavorite = tree.favorites.Contains(node.option.value);
isFavorite = GUI.Toggle(starPosition, isFavorite, GUIContent.none, Styles.star);
if (EditorGUI.EndChangeCheck())
{
if (isFavorite)
{
tree.favorites.Add(node.option.value);
}
else
{
tree.favorites.Remove(node.option.value);
}
tree.OnFavoritesChange();
UpdateFavorites();
}
}
if (!isAnimating)
{
if (e.type == EventType.MouseDown && e.button == (int)MouseButton.Left && optionPosition.Contains(e.mousePosition))
{
e.Use();
parent.selectedIndex = i;
SelectChild(node);
GUIUtility.ExitGUI();
}
}
}
EditorGUIUtility.SetIconSize(default(Vector2));
GUILayout.EndScrollView();
if (scrollToSelected && e.type == EventType.Repaint)
{
scrollToSelected = false;
var lastRect = GUILayoutUtility.GetLastRect();
if (selectedOptionPosition.yMax - lastRect.height > parent.scroll.y)
{
var scroll = parent.scroll;
scroll.y = selectedOptionPosition.yMax - lastRect.height;
parent.scroll = scroll;
requireRepaint = true;
}
if (selectedOptionPosition.y < parent.scroll.y)
{
var scroll = parent.scroll;
scroll.y = selectedOptionPosition.y;
parent.scroll = scroll;
requireRepaint = true;
}
}
}
const int footerWidthMargin = 2;
private void OnFooterGUI()
{
var footerPosition = new Rect
(
1,
height - 1,
position.width - footerWidthMargin,
footerHeight
);
var backgroundPosition = footerPosition;
backgroundPosition.height += 1;
if (e.type == EventType.Repaint)
{
Styles.footerBackground.Draw(backgroundPosition, false, false, false, false);
}
if (activeNode != null && activeNode.option.hasFooter)
{
activeNode.option.OnFooterGUI(footerPosition);
}
}
private void HandleKeyboard()
{
if (e.type == EventType.KeyDown)
{
if (e.keyCode == KeyCode.DownArrow)
{
activeSelectedIndex = Mathf.Clamp(activeSelectedIndex + 1, 0, activeNodes.Count - 1);
scrollToSelected = true;
e.Use();
}
else if (e.keyCode == KeyCode.UpArrow)
{
activeSelectedIndex = Mathf.Clamp(activeSelectedIndex - 1, 0, activeNodes.Count);
scrollToSelected = true;
e.Use();
}
else if ((e.keyCode == KeyCode.Return || e.keyCode == KeyCode.KeypadEnter) && !activeParent.isLoading)
{
SelectChild(activeNode);
e.Use();
}
else if ((e.keyCode == KeyCode.LeftArrow || e.keyCode == KeyCode.Backspace) && activeParent != activeRoot)
{
SelectParent();
e.Use();
}
else if (e.keyCode == KeyCode.RightArrow && !activeParent.isLoading)
{
EnterChild(activeNode);
e.Use();
}
else if (e.keyCode == KeyCode.Escape)
{
Close();
e.Use();
}
}
}
#endregion
#region Threading
private readonly object guiLock = new object();
private void ExecuteTask(Action task)
{
Ensure.That(nameof(task)).IsNotNull(task);
if (!tree.multithreaded)
{
task();
ClearProgressBar();
return;
}
lock (queue)
{
queue.Enqueue(task);
}
if (workerThread == null)
{
workerThread = new Thread(Work);
workerThread.Name = "Fuzzy Window";
workerThread.Start();
}
}
private readonly ConcurrentQueue<Action> queue = new ConcurrentQueue<Action>();
private Thread workerThread;
private void Work()
{
while (true)
{
if (!queue.TryDequeue(out var task))
{
break;
}
try
{
task();
}
catch (OperationCanceledException) { }
catch (ThreadAbortException) { }
catch (Exception ex)
{
Debug.LogException(ex);
}
finally
{
ClearProgressBar();
requireRepaint = true;
}
}
workerThread = null;
}
private string progressText;
private float progress;
private bool showProgress;
public void DisplayProgressBar(string text, float progress)
{
progressText = text;
this.progress = progress;
showProgress = true;
}
public void DisplayProgressBar(float progress)
{
DisplayProgressBar(null, progress);
}
public static void ClearProgressBar()
{
if (instance == null)
{
return;
}
instance.progressText = null;
instance.progress = 0;
instance.showProgress = false;
}
#endregion
}
}