Files
2025-07-04 20:33:06 +03:00

607 lines
21 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Meta.Voice.Hub.Attributes;
using Meta.Voice.Hub.Interfaces;
using Meta.Voice.Hub.UIComponents;
using Meta.Voice.Hub.Utilities;
using Meta.Voice.TelemetryUtilities;
using UnityEditor;
using UnityEngine;
namespace Meta.Voice.Hub
{
public class MetaHub : EditorWindow
{
[SerializeField] private Texture2D _icon;
[SerializeField] private Texture2D _logoImage;
private int _leftPanelWidth = 200;
private List<MetaHubContext> _contexts = new List<MetaHubContext>();
private Dictionary<string, MetaHubContext> _contextMap = new Dictionary<string, MetaHubContext>();
private List<PageGroup> _pageGroups = new List<PageGroup>();
private Dictionary<MetaHubContext, PageGroup> _pageGroupMap = new Dictionary<MetaHubContext, PageGroup>();
public MetaHubContext PrimaryContext
{
get
{
if (ContextFilter.Count > 0)
{
var filter = ContextFilter.First();
if (_contextMap.TryGetValue(filter, out var context))
{
return context;
}
}
return _contexts[0];
}
}
public GUIContent TitleContent => new GUIContent(PrimaryContext.Title, PrimaryContext.Icon);
public Texture2D LogoImage => PrimaryContext.LogoImage ? PrimaryContext.LogoImage : _logoImage;
public const string DEFAULT_CONTEXT = "";
public virtual List<string> ContextFilter => new List<string>();
private string _selectedPageId;
public string SelectedPage
{
get
{
if (null == _selectedPageId)
{
_selectedPageId = SessionState.GetString(SessionKeySelPage, null);
}
return _selectedPageId;
}
set
{
if (_selectedPageId != value)
{
_selectedPageId = value;
Telemetry.LogInstantEvent(Telemetry.TelemetryEventId.OpenUi,
new Dictionary<Telemetry.AnnotationKey, string>()
{
{ Telemetry.AnnotationKey.PageId, _selectedPageId }
});
if (!string.IsNullOrEmpty(value))
{
SessionState.SetString(SessionKeySelPage, value);
}
}
}
}
private string SessionKeySelPage => GetType().Namespace + "." + GetType().Name + "::SelectedPage";
private PageGroup _rootPageGroup;
private class PageGroup
{
private MetaHubContext _context;
private List<PageReference> _pages = new List<PageReference>();
private HashSet<string> _addedPages = new HashSet<string>();
private FoldoutHierarchy<PageReference> _foldoutHierarchy = new FoldoutHierarchy<PageReference>();
private readonly Action<PageReference> _onDrawPage;
public MetaHubContext Context => _context;
public IEnumerable<PageReference> Pages => _pages;
public int PageCount => _pages.Count;
public FoldoutHierarchy<PageReference> Hierarchy => _foldoutHierarchy;
public PageGroup(MetaHubContext context, Action<PageReference> onDrawPage)
{
_context = context;
_onDrawPage = onDrawPage;
}
public void AddPage(PageReference page)
{
var pageId = page.PageId;
if (!_addedPages.Contains(pageId))
{
_addedPages.Add(pageId);
_pages.Add(page);
var prefix = page.info.Prefix?.Trim(new char[] { '/' });
if (prefix.Length > 0)
{
prefix += "/";
}
var path = "/" + prefix + page.info.Name;
_foldoutHierarchy.Add(path, new FoldoutHierarchyItem<PageReference> {
path = path,
item = page,
onDraw = _onDrawPage
});
}
}
public void Sort()
{
Sort(_pages);
}
public void Sort(List<PageReference> pages)
{
pages.Sort((a, b) =>
{
int compare = a.info.Priority.CompareTo(b.info.Priority);
if (compare == 0) compare = string.Compare(a.info.Name, b.info.Name);
return compare;
});
_foldoutHierarchy = new FoldoutHierarchy<PageReference>();
foreach (var page in _pages)
{
var path = "/" + page.info.Prefix + page.info.Name;
_foldoutHierarchy.Add(path, new FoldoutHierarchyItem<PageReference> {
path = path,
item = page,
onDraw = _onDrawPage
});
}
}
}
private struct PageReference
{
public IMetaHubPage page;
public IPageInfo info;
public string PageId => info.Context + "::" + info.Name;
}
private string _searchString = "";
private IMetaHubPage _selectedPage;
private Vector2 _scroll;
private Vector2 _leftScroll;
private IMetaHubPage ActivePage
{
get => _selectedPage;
set
{
if (_selectedPage != value)
{
if (null != _selectedPage)
{
DisableSelectedPage(_selectedPage);
}
_selectedPage = value;
EnableSelectedPage(_selectedPage);
}
}
}
private void InvokeLifecyle(IMetaHubPage page, string lifecycleMethod)
{
if (null == page) return;
var method = page.GetType()
.GetMethod(lifecycleMethod, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
method?.Invoke(page, new object[0]);
}
private void DisableSelectedPage(IMetaHubPage page)
{
InvokeLifecyle(page, "OnDisable");
}
private void EnableSelectedPage(IMetaHubPage page)
{
if (null == page) return;
var method = page.GetType()
.GetMethod("OnWindow", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
method?.Invoke(page, new object[] {this});
InvokeLifecyle(page, "OnEnable");
}
private void OnSelectionChange()
{
InvokeLifecyle(ActivePage, "OnSelectionChange");
}
protected virtual void OnEnable()
{
UpdateContextFilter();
minSize = new Vector2(400, 400);
}
private void OnDisable()
{
if (null != _selectedPage)
{
ActivePage = null;
}
}
protected void AddChildContexts(List<string> filter)
{
var parents = new HashSet<string>();
foreach (var parent in filter)
{
parents.Add(parent);
}
var contexts = ContextFinder.FindAllContextAssets<MetaHubContext>();
foreach (var context in contexts)
{
if (!context) continue;
if (null == context.ParentContexts) continue;
foreach (var parentContext in context.ParentContexts)
{
if(parents.Contains(parentContext)) filter.Add(context.Name);
}
}
}
public void UpdateContextFilter()
{
if(null == _rootPageGroup) _rootPageGroup = new PageGroup(null, DrawPageEntry);
_contexts = ContextFinder.FindAllContextAssets<MetaHubContext>();
_contexts.Sort((a, b) => a.Priority.CompareTo(b.Priority));
foreach (var context in _contexts)
{
_contextMap[context.Name] = context;
var pageGroup = new PageGroup(context, DrawPageEntry);
if (!_pageGroupMap.ContainsKey(context))
{
_pageGroups.Add(pageGroup);
_pageGroupMap[context] = pageGroup;
foreach (var soPage in context.ScriptableObjectReflectionPages)
{
var pages = PageFinder.FindPages(soPage.scriptableObjectType);
foreach (var so in pages)
{
var page = new ScriptableObjectPage(so, context.Name, prefix: soPage.namePrefix, priority: soPage.priorityModifier);
AddPage(new PageReference
{
page = page,
info = page
});
}
}
}
}
foreach (var page in ContextFinder.FindAllContextAssets<MetaHubPage>())
{
AddPage(new PageReference
{
page = page,
info = page
});
}
foreach (var pageType in PageFinder.FindPages())
{
var pageInfo = PageFinder.GetPageInfo(pageType);
if (pageInfo is MetaHubPageScriptableObjectAttribute)
{
var pages = PageFinder.FindPages(pageType);
foreach (var page in pages)
{
var soPage = new ScriptableObjectPage(page, pageInfo);
AddPage(new PageReference
{
page = soPage,
info = soPage
});
}
}
else
{
IMetaHubPage page;
if (pageType.IsSubclassOf(typeof(ScriptableObject)))
{
page = (IMetaHubPage) ScriptableObject.CreateInstance(pageType);
}
else
{
page = Activator.CreateInstance(pageType) as IMetaHubPage;
}
if(page is IPageInfo info) AddPage(new PageReference { page = page, info = info});
else AddPage(new PageReference { page = page, info = pageInfo});
}
}
// Sort the pages by priority then alpha
foreach (var group in _pageGroupMap.Values)
{
group.Sort();
}
}
private void AddPage(PageReference page)
{
if (string.IsNullOrEmpty(page.info.Context)) _rootPageGroup.AddPage(page);
else if (_contextMap.TryGetValue(page.info.Context, out var groupKey)
&& _pageGroupMap.TryGetValue(groupKey, out var group))
{
group.AddPage(page);
}
}
protected virtual void OnGUI()
{
titleContent = TitleContent;
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
_searchString = EditorGUILayout.TextField(_searchString, EditorStyles.toolbarSearchField);
GUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
DrawLeftPanel();
DrawRightPanel();
EditorGUILayout.EndHorizontal();
}
private void DrawLeftPanel()
{
EditorGUILayout.BeginVertical(GUILayout.Width(_leftPanelWidth));
var logo = LogoImage;
// Draw logo image
if (logo)
{
float aspectRatio = logo.width / (float) logo.height;
GUILayout.Box(logo, GUILayout.Width(_leftPanelWidth), GUILayout.Height(_leftPanelWidth / aspectRatio));
}
_leftScroll = GUILayout.BeginScrollView(_leftScroll);
DrawPageGroup(_rootPageGroup);
foreach (var context in _pageGroups)
{
DrawPageGroup(context);
}
GUILayout.EndScrollView();
EditorGUILayout.EndVertical();
}
private void DrawPageGroup(PageGroup group)
{
if (!IsGroupVisible(group)) return;
var searchMatchedGroupContext = ContextFilter.Count != 1 && IsGroupInSearch(group);
List<PageReference> pages = new List<PageReference>();
if (!string.IsNullOrEmpty(_searchString) && !searchMatchedGroupContext)
{
foreach (var page in group.Pages)
{
if (PageInSearch(page))
{
pages.Add(page);
}
}
}
if (ContextFilter.Count != 1 && (string.IsNullOrEmpty(_searchString) || pages.Count > 0))
{
if (group.Context != PrimaryContext && null != group.Context &&
(string.IsNullOrEmpty(_searchString) && group.PageCount > 0 || pages.Count > 0) &&
!string.IsNullOrEmpty(group.Context.Name) && group.Context.ShowPageGroupTitle)
{
GUILayout.Space(8);
GUILayout.Label(group.Context.Name, EditorStyles.boldLabel);
}
}
if(!string.IsNullOrEmpty(_searchString))
{
for (int i = 0; i < pages.Count; i++)
{
DrawPageEntry(pages[i]);
}
}
else
{
group.Hierarchy.Draw();
}
}
private bool PageInSearch(PageReference page)
{
#if UNITY_2021_1_OR_NEWER
return page.info.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
#else
return page.info.Name.ToLower().Contains(_searchString.ToLower());
#endif
}
private bool IsGroupInSearch(PageGroup group)
{
#if UNITY_2021_1_OR_NEWER
return group.Context && group.Context.Name.Contains(_searchString,
StringComparison.OrdinalIgnoreCase);
#else
return group.Context && group.Context.Name.ToLower().Contains(_searchString.ToLower());
#endif
}
private bool IsGroupVisible(PageGroup group)
{
return group.PageCount > 0 &&
ContextFilter.Count == 0 && (!group.Context || group.Context.AllowWithoutContextFilter) ||
ContextFilter.Contains(group.Context ? group.Context.Name : "");
}
private void DrawPageEntry(PageReference page)
{
GUIStyle optionStyle = new GUIStyle(GUI.skin.label);
optionStyle.normal.background = null;
optionStyle.normal.textColor = _selectedPage == page.page ? Color.white : GUI.skin.label.normal.textColor;
if (string.IsNullOrEmpty(SelectedPage))
{
// TODO: We will need to improve this logic.
if (!string.IsNullOrEmpty(SelectedPage) && page.PageId == SelectedPage) ActivePage = page.page;
else if(string.IsNullOrEmpty(SelectedPage)) ActivePage = page.page;
SelectedPage = page.PageId;
}
else if (null == ActivePage)
{
if (page.PageId == SelectedPage)
{
ActivePage = page.page;
}
}
EditorGUILayout.BeginHorizontal();
{
Rect optionRect = GUILayoutUtility.GetRect(GUIContent.none, optionStyle, GUILayout.ExpandWidth(true), GUILayout.Height(20));
bool isHover = optionRect.Contains(Event.current.mousePosition);
if (isHover)
{
EditorGUIUtility.AddCursorRect(optionRect, MouseCursor.Link);
}
Color backgroundColor;
if (page.page == _selectedPage)
{
backgroundColor = EditorGUIUtility.isProSkin ? new Color(0.22f, 0.44f, 0.88f) : new Color(0.24f, 0.48f, 0.90f);
}
else
{
backgroundColor = Color.clear;
}
EditorGUI.DrawRect(optionRect, backgroundColor);
GUI.Label(optionRect, new GUIContent(page.info.Name, page.info.Name), optionStyle);
if (Event.current.type == EventType.MouseDown && isHover)
{
ActivePage = page.page;
SelectedPage = page.PageId;
Event.current.Use();
}
}
EditorGUILayout.EndHorizontal();
}
protected virtual void DrawRightPanel()
{
// Create a GUIStyle with a darker background color
GUIStyle darkBackgroundStyle = new GUIStyle();
Texture2D backgroundTexture = new Texture2D(1, 1);
backgroundTexture.SetPixel(0, 0, new Color(0f, 0f, 0f, .25f));
backgroundTexture.Apply();
darkBackgroundStyle.normal.background = backgroundTexture;
// Apply the dark background style to the right panel
EditorGUILayout.BeginVertical(darkBackgroundStyle, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
if (_selectedPage is ScriptableObjectPage soPage)
{
if(soPage.Editor is IOverrideSize size && Event.current.type == EventType.Layout) {
size.OverrideWidth = EditorGUIUtility.currentViewWidth - _leftPanelWidth;
}
}
_selectedPage?.OnGUI();
EditorGUILayout.EndVertical();
}
public static T ShowWindow<T>(params string[] contexts) where T : MetaHub
{
var window = EditorWindow.GetWindow<T>();
window.ActivePage = null;
window.titleContent = new GUIContent("Meta Hub");
window.UpdateContextFilter();
window.Show();
return window;
}
}
internal class ScriptableObjectPage : IMetaHubPage, IPageInfo
{
private readonly ScriptableObject _page;
private string _context;
private Editor _editor;
private string _name;
private string _prefix = "";
private int _priority;
public string Name => _name;
public string Prefix => _prefix;
public string Context => _context;
public Editor Editor => _editor;
public int Priority => _priority;
public ScriptableObjectPage(ScriptableObject page, MetaHubPageAttribute pageInfo)
{
_page = page;
_context = pageInfo.Context;
_priority = pageInfo.Priority;
_prefix = pageInfo.Prefix;
UpdatePageInfo();
}
public ScriptableObjectPage(ScriptableObject page, string context, string prefix = "", int priority = 0)
{
_page = page;
_context = context;
_priority = priority;
_prefix = prefix;
UpdatePageInfo();
}
private void UpdatePageInfo()
{
if (_page is IPageInfo info)
{
if (!string.IsNullOrEmpty(info.Name)) _name = info.Name;
if (!string.IsNullOrEmpty(info.Context)) _context = info.Context;
if (!string.IsNullOrEmpty(info.Prefix)) _prefix = info.Prefix;
if (info.Priority != 0) _priority = info.Priority;
}
else
{
_name = _page.name;
}
}
public void OnGUI()
{
if (_page)
{
// Create an editor for the assigned ScriptableObject
if (_editor == null || _editor.target != _page)
{
_editor = Editor.CreateEditor(_page);
}
// Render the ScriptableObject with its default editor
_editor.OnInspectorGUI();
}
}
}
}