I am working on custom enum popup with search and facing next problem. When clicking button to show popup list of enums I can hover over that list and select items inside, but also I can as always move mouse outside popup rect and move around. And when I hover over other GUI element it reacts to that movement (hover highlight or so happens). I remembered that default Unity's enum don't have this feature and it blocks all events outside popup's rect exclude outside mouse click to close this popup.
So... My question is: How can I implement this feature in my popup window? I look into Unity's source code and don't find anything related to this. Except one moment when they are using internal function "GUI.GrabMouseControl(id);"
There are this internal function for enum button where they block mouse control somehow:
// A button that returns true on mouse down - like a popup button
internal static bool DropdownButton(int id, Rect position, GUIContent content, GUIStyle style)
{
Event evt = Event.current;
switch (evt.type)
{
case EventType.Repaint:
var hovered = position.Contains(Event.current.mousePosition);
if (showMixedValue)
{
BeginHandleMixedValueContentColor();
style.Draw(position, s_MixedValueContent, id, false, hovered);
EndHandleMixedValueContentColor();
}
else
style.Draw(position, content, id, false, hovered);
break;
case EventType.MouseDown:
if (GUIUtility.HitTest(position, evt) && evt.button == 0)
{
GUI.GrabMouseControl(id);
Event.current.Use();
return true;
}
break;
case EventType.MouseUp:
if (GUI.HasMouseControl(id))
{
GUI.ReleaseMouseControl();
Event.current.Use();
}
break;
case EventType.KeyDown:
if (GUIUtility.keyboardControl == id && evt.character == ' ')
{
Event.current.Use();
return true;
}
break;
}
return false;
}
I think that may be that magic thing to block mouse hovering. Also I think that GUIUtility.hotControll is second canditate to solve my issue. But I dont know how properly use it in my OnGUI() method. I am trying to it and get not correct result, but I blocked hovering outside popup's rect. But it always spam console with these logs: Should not grab hot control with an active capture. So I dropped this idea with my implementation, cause of these logs...
There are my code of popup window content:
using System;
using UnityEditor; using UnityEngine;
public class SearchablePopup : PopupWindowContent { #region Popup Properties
private const float SymbolVerticalOffset = 0.75f;
private const int ClearButtonSize = 12;
private const string SearchControl = "searchablePopup_ctrl";
#endregion
public static void Show(Rect from, Type type, int current,
Action<int> onSelectionMade, int elementsToShow, int elementHeight,
int searchHeight, Texture backgroundTexture,
GUIStyle searchStyle, GUIStyle clearButtonStyle, GUIStyle elementStyle,
Color hover, Color selected, Color marker)
{
if (type.IsEnum is false) return;
var window = new SearchablePopup(type, current, onSelectionMade,
(int)from.width, elementsToShow, elementHeight, searchHeight,
backgroundTexture, searchStyle, clearButtonStyle, elementStyle,
hover, selected, marker);
PopupWindow.Show(from, window);
}
private readonly FilteredList _filteredList;
private readonly Action<int> _onSelectionMade;
private readonly int _width;
private readonly int _elementsToShow;
private readonly int _rowHeight;
private readonly int _searchHeight;
private readonly float _clearButtonPadding;
private readonly bool _clearButtonValid;
private readonly GUIStyle _searchStyle;
private readonly GUIStyle _searchTooltipStyle;
private readonly GUIStyle _clearButtonStyle;
private readonly GUIStyle _elementStyle;
private readonly Texture _backgroundTexture;
private readonly Texture _hoverTexture;
private readonly Texture _selectedTexture;
private readonly Texture _selectedCircleTexture;
private int _current;
private int _hover;
private int _keyboardIndex;
private int _scroll;
private int _controlHash = "SearchablePopup".GetHashCode();
private Vector2 _scrollPosition;
private Vector2 _clickPosition;
private bool _clickedThisFrame;
public SearchablePopup(Type enumType, int current, Action<int> onSelectionMade,
int width, int elementsToShow, int rowHeight, int searchHeight, Texture backgroundTexture,
GUIStyle searchStyle, GUIStyle clearButtonStyle, GUIStyle elementStyle,
Color hover, Color selected, Color marker)
{
_filteredList = new FilteredList(enumType, current);
_hoverTexture = SmallTexture(hover);
_selectedTexture = SmallTexture(selected);
_selectedCircleTexture = DrawCircle(4, marker);
_current = current;
_hover = _current;
_scroll = _current;
_keyboardIndex = _filteredList.CurrentIndex;
_onSelectionMade = onSelectionMade;
_elementsToShow = elementsToShow;
_width = width;
_rowHeight = rowHeight;
_searchHeight = searchHeight;
_backgroundTexture = backgroundTexture;
_searchStyle = searchStyle;
_clearButtonValid = clearButtonStyle != null;
_clearButtonStyle = clearButtonStyle;
_clearButtonPadding = _clearButtonValid ? clearButtonStyle.padding.left : 4;
_elementStyle = elementStyle;
if (_clearButtonValid is false)
{
_elementStyle = new GUIStyle(_elementStyle);
_elementStyle.alignment = TextAnchor.MiddleLeft;
}
_searchTooltipStyle = new GUIStyle(_searchStyle);
_searchTooltipStyle.normal.textColor *= 0.75f;
_searchTooltipStyle.hover.textColor = _searchTooltipStyle.normal.textColor;
_searchTooltipStyle.fontSize = (int)(_searchTooltipStyle.fontSize * 0.75f);
}
public override Vector2 GetWindowSize()
{
float height = Mathf.Min(
_filteredList.Entries.Count, _elementsToShow) * _rowHeight
+ _searchHeight;
//Strange hot-fix for Unity's Scrollview:
//We are increasing total height of our window's rect
//to be a litte bigger than actual content because
//if Scrollview's height >= Window's height than scrollbar is displayed
//what we don't want to see if our current count of entires is less than show amount
if (_filteredList.Entries.Count < _elementsToShow)
height += 0.25f;
return new Vector2(_width, height);
}
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 void OnGUI(Rect rect)
{
GUI.DrawTexture(rect, _backgroundTexture);
if (Event.current.type == EventType.MouseDown)
{
_clickedThisFrame = true;
_clickPosition = Event.current.mousePosition;
}
Rect search = new Rect(0, 0, rect.width, _searchHeight);
Rect clear = Rect.MinMaxRect(search.xMax - ClearButtonSize - _clearButtonPadding,
search.center.y - ClearButtonSize * 0.5f, rect.xMax - _clearButtonPadding,
search.center.y + ClearButtonSize * 0.5f);
Rect scroll = Rect.MinMaxRect(0, search.yMax, rect.xMax, rect.yMax);
HandleKeyboardInput();
DrawSearch(search, clear);
DrawScroll(scroll);
_clickedThisFrame = false;
}
private void DrawSearch(Rect search, Rect clear)
{
GUI.FocusControl(SearchControl);
GUI.SetNextControlName(SearchControl);
var filter = TextfieldWithTooltip(search, _filteredList.Filter,
"Type to search...", _searchStyle, _searchTooltipStyle);
if (_filteredList.Refresh(filter))
{
_scrollPosition = Vector2.zero;
}
if (string.IsNullOrEmpty(_filteredList.Filter) is false)
{
if (_clearButtonValid)
{
GUI.Box(clear, GUIContent.none, _clearButtonStyle);
}
else
{
GUI.Box(clear, "x", GUI.skin.label);
}
if (_clickedThisFrame && clear.Contains(_clickPosition))
{
_filteredList.Refresh("");
_scrollPosition = Vector2.zero;
}
}
}
private void DrawScroll(Rect scroll)
{
Rect contentRect = new Rect(0, 0,
scroll.width - GUI.skin.verticalScrollbar.fixedWidth,
_filteredList.Entries.Count * _rowHeight);
_scrollPosition = GUI.BeginScrollView(scroll, _scrollPosition, contentRect);
Rect element = new Rect(0, 0, scroll.width, _rowHeight);
var eventType = Event.current.type;
for (int i = 0; i < _filteredList.Entries.Count; i++)
{
if (_scroll == _filteredList.Entries[i].Value && (eventType == EventType.Repaint || eventType == EventType.Layout))
{
GUI.ScrollTo(element);
_scrollPosition.x = 0;
_scroll = -1;
}
if (element.Contains(Event.current.mousePosition))
{
if (Event.current.type == EventType.MouseMove ||
Event.current.type == EventType.ScrollWheel)
{
_hover = _filteredList.Entries[i].Value;
_keyboardIndex = i;
}
if (Event.current.type == EventType.MouseDown)
{
_onSelectionMade(_filteredList.Entries[i].Value);
EditorWindow.focusedWindow.Close();
}
}
DrawElement(element, _filteredList.Entries[i].Value, i);
element.y = element.yMax;
}
GUI.EndScrollView();
}
private void DrawElement(Rect rect, int value, int index)
{
if (value == _current)
{
DrawBox(rect, _selectedTexture, _selectedCircleTexture);
rect.xMin += _selectedCircleTexture.width * 2f;
}
else if (value == _hover)
{
DrawBox(rect, _hoverTexture);
}
GUI.Label(rect, _filteredList.Entries[index].Content, _elementStyle);
}
private void DrawBox(Rect rect, Texture texture, Texture symbol = null)
{
GUI.DrawTexture(rect, texture);
if (symbol == null) return;
rect.xMin += symbol.width * 0.5f;
rect.y = rect.center.y - symbol.height * 0.5f + SymbolVerticalOffset;
rect.width = symbol.width;
rect.height = symbol.height;
GUI.DrawTexture(rect, symbol);
}
private void HandleKeyboardInput()
{
if (Event.current.isKey is false || Event.current.type != EventType.KeyDown) return;
var keyCode = Event.current.keyCode;
if (keyCode == KeyCode.Escape)
{
EditorWindow.focusedWindow.Close();
return;
}
if (keyCode == KeyCode.Return)
{
_onSelectionMade(_filteredList.Entries[_keyboardIndex].Value);
EditorWindow.focusedWindow.Close();
return;
}
if (keyCode == KeyCode.DownArrow)
{
_keyboardIndex = Mathf.Min(_filteredList.Entries.Count - 1, _keyboardIndex + 1);
_hover = _filteredList.Entries[_keyboardIndex].Value;
Event.current.Use();
_scroll = _hover;
}
else if (keyCode == KeyCode.UpArrow)
{
_keyboardIndex = Mathf.Max(0, _keyboardIndex - 1);
_hover = _filteredList.Entries[_keyboardIndex].Value;
GUIUtility.hotControl = 0;
Event.current.Use();
_scroll = _hover;
}
}
#region Helpers
public static Rect Shrink(Rect rect, float amount)
{
return new Rect(rect.x + amount, rect.y + amount, rect.width - amount * 2, rect.height - amount * 2);
}
public static Texture SmallTexture(string hex)
{
Texture2D texture = new Texture2D(4, 4);
var color = HexToRGB(hex);
for (int i = 0; i < texture.width; i++)
{
for (int j = 0; j < texture.height; j++)
{
texture.SetPixel(i, j, color);
}
}
texture.Apply();
return texture;
}
public static Texture SmallTexture(Color color)
{
Texture2D texture = new Texture2D(4, 4);
for (int i = 0; i < texture.width; i++)
{
for (int j = 0; j < texture.height; j++)
{
texture.SetPixel(i, j, color);
}
}
texture.Apply();
return texture;
}
public static Texture DrawCircle(int radius, string hex)
{
Texture2D texture = new Texture2D(radius * 2, radius * 2);
var color = HexToRGB(hex);
float cX = radius;
float cY = radius;
float sizeSquared = radius * radius;
for (int i = 0; i < texture.width; i++)
{
for (int j = 0; j < texture.height; j++)
{
if ((cX - i) * (cX - i) + (cY - j) * (cY - j) < sizeSquared)
{
texture.SetPixel(i, j, color);
}
else texture.SetPixel(i, j, Color.clear);
}
}
texture.Apply();
return texture;
}
public static Texture DrawCircle(int radius, Color color)
{
Texture2D texture = new Texture2D(radius * 2, radius * 2);
float cX = radius;
float cY = radius;
float sizeSquared = radius * radius;
for (int i = 0; i < texture.width; i++)
{
for (int j = 0; j < texture.height; j++)
{
if ((cX - i) * (cX - i) + (cY - j) * (cY - j) < sizeSquared)
{
texture.SetPixel(i, j, color);
}
else texture.SetPixel(i, j, Color.clear);
}
}
texture.Apply();
return texture;
}
public static Color HexToRGB(string hex)
{
hex = hex.TrimStart('#');
if (hex.Length != 6) return Color.black;
int r = Int16.Parse(hex.Substring(0, 2),
System.Globalization.NumberStyles.AllowHexSpecifier);
int g = Int16.Parse(hex.Substring(2, 2),
System.Globalization.NumberStyles.AllowHexSpecifier);
int b = Int16.Parse(hex.Substring(4, 2),
System.Globalization.NumberStyles.AllowHexSpecifier);
return new Color(r / 255f, g / 255f, b / 255f);
}
public static string TextfieldWithTooltip(Rect rect, string text, string tooltip,
GUIStyle textfieldStyle, GUIStyle tooltipStyle)
{
text = GUI.TextField(rect, text, textfieldStyle);
if (text.Length == 0)
GUI.Label(rect, tooltip, tooltipStyle);
return text;
}
#endregion
}
I will be realy glad to see some advices or straight anwsers on this problem is someone can solve it :) Thank you!