MetaHub.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. /*
  2. * Copyright (c) Meta Platforms, Inc. and affiliates.
  3. * All rights reserved.
  4. *
  5. * This source code is licensed under the license found in the
  6. * LICENSE file in the root directory of this source tree.
  7. */
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Diagnostics.Eventing.Reader;
  11. using System.Linq;
  12. using System.Reflection;
  13. using Meta.Voice.Hub.Attributes;
  14. using Meta.Voice.Hub.Interfaces;
  15. using Meta.Voice.Hub.UIComponents;
  16. using Meta.Voice.Hub.Utilities;
  17. using UnityEditor;
  18. using UnityEngine;
  19. namespace Meta.Voice.Hub
  20. {
  21. public class MetaHub : EditorWindow
  22. {
  23. [SerializeField] private Texture2D _icon;
  24. [SerializeField] private Texture2D _logoImage;
  25. private int _leftPanelWidth = 200;
  26. private List<string> _contextFilter = new List<string>();
  27. private List<MetaHubContext> _contexts = new List<MetaHubContext>();
  28. private Dictionary<string, MetaHubContext> _contextMap = new Dictionary<string, MetaHubContext>();
  29. private List<PageGroup> _pageGroups = new List<PageGroup>();
  30. private Dictionary<MetaHubContext, PageGroup> _pageGroupMap = new Dictionary<MetaHubContext, PageGroup>();
  31. public MetaHubContext PrimaryContext
  32. {
  33. get
  34. {
  35. if (ContextFilter.Count > 0)
  36. {
  37. var filter = ContextFilter.First();
  38. if (_contextMap.TryGetValue(filter, out var context))
  39. {
  40. return context;
  41. }
  42. }
  43. return _contexts[0];
  44. }
  45. }
  46. public GUIContent TitleContent => new GUIContent(PrimaryContext.Title, PrimaryContext.Icon);
  47. public Texture2D LogoImage => PrimaryContext.LogoImage ? PrimaryContext.LogoImage : _logoImage;
  48. public const string DEFAULT_CONTEXT = "";
  49. public virtual List<string> ContextFilter => _contextFilter;
  50. public string SelectedPage { get; set; } = "";
  51. private PageGroup _rootPageGroup;
  52. private class PageGroup
  53. {
  54. private MetaHubContext _context;
  55. private List<PageReference> _pages = new List<PageReference>();
  56. private HashSet<string> _addedPages = new HashSet<string>();
  57. private FoldoutHierarchy<PageReference> _foldoutHierarchy = new FoldoutHierarchy<PageReference>();
  58. private readonly Action<PageReference> _onDrawPage;
  59. public MetaHubContext Context => _context;
  60. public IEnumerable<PageReference> Pages => _pages;
  61. public int PageCount => _pages.Count;
  62. public FoldoutHierarchy<PageReference> Hierarchy => _foldoutHierarchy;
  63. public PageGroup(MetaHubContext context, Action<PageReference> onDrawPage)
  64. {
  65. _context = context;
  66. _onDrawPage = onDrawPage;
  67. }
  68. public void AddPage(PageReference page)
  69. {
  70. var pageId = page.PageId;
  71. if (!_addedPages.Contains(pageId))
  72. {
  73. _addedPages.Add(pageId);
  74. _pages.Add(page);
  75. var prefix = page.info.Prefix?.Trim(new char[] { '/' });
  76. if (prefix.Length > 0)
  77. {
  78. prefix += "/";
  79. }
  80. var path = "/" + prefix + page.info.Name;
  81. _foldoutHierarchy.Add(path, new FoldoutHierarchyItem<PageReference> {
  82. path = path,
  83. item = page,
  84. onDraw = _onDrawPage
  85. });
  86. }
  87. }
  88. public void Sort()
  89. {
  90. Sort(_pages);
  91. }
  92. public void Sort(List<PageReference> pages)
  93. {
  94. pages.Sort((a, b) =>
  95. {
  96. int compare = a.info.Priority.CompareTo(b.info.Priority);
  97. if (compare == 0) compare = string.Compare(a.info.Name, b.info.Name);
  98. return compare;
  99. });
  100. _foldoutHierarchy = new FoldoutHierarchy<PageReference>();
  101. foreach (var page in _pages)
  102. {
  103. var path = "/" + page.info.Prefix + page.info.Name;
  104. _foldoutHierarchy.Add(path, new FoldoutHierarchyItem<PageReference> {
  105. path = path,
  106. item = page,
  107. onDraw = _onDrawPage
  108. });
  109. }
  110. }
  111. }
  112. private struct PageReference
  113. {
  114. public IMetaHubPage page;
  115. public IPageInfo info;
  116. public string PageId => info.Context + "::" + info.Name;
  117. }
  118. private string _searchString = "";
  119. private IMetaHubPage _selectedPage;
  120. private Vector2 _scroll;
  121. private Vector2 _leftScroll;
  122. private void OnEnable()
  123. {
  124. UpdateContextFilter();
  125. minSize = new Vector2(400, 400);
  126. }
  127. public void UpdateContextFilter()
  128. {
  129. if(null == _rootPageGroup) _rootPageGroup = new PageGroup(null, DrawPageEntry);
  130. _contexts = ContextFinder.FindAllContextAssets<MetaHubContext>();
  131. _contexts.Sort((a, b) => a.Priority.CompareTo(b.Priority));
  132. foreach (var context in _contexts)
  133. {
  134. _contextMap[context.Name] = context;
  135. var pageGroup = new PageGroup(context, DrawPageEntry);
  136. if (!_pageGroupMap.ContainsKey(context))
  137. {
  138. _pageGroups.Add(pageGroup);
  139. _pageGroupMap[context] = pageGroup;
  140. foreach (var soPage in context.ScriptableObjectReflectionPages)
  141. {
  142. var pages = PageFinder.FindPages(soPage.scriptableObjectType);
  143. foreach (var so in pages)
  144. {
  145. var page = new ScriptableObjectPage(so, context.Name, prefix: soPage.namePrefix, priority: soPage.priorityModifier);
  146. AddPage(new PageReference
  147. {
  148. page = page,
  149. info = page
  150. });
  151. }
  152. }
  153. }
  154. }
  155. foreach (var page in ContextFinder.FindAllContextAssets<MetaHubPage>())
  156. {
  157. AddPage(new PageReference
  158. {
  159. page = page,
  160. info = page
  161. });
  162. }
  163. foreach (var pageType in PageFinder.FindPages())
  164. {
  165. var pageInfo = PageFinder.GetPageInfo(pageType);
  166. if (pageInfo is MetaHubPageScriptableObjectAttribute)
  167. {
  168. var pages = PageFinder.FindPages(pageType);
  169. foreach (var page in pages)
  170. {
  171. var soPage = new ScriptableObjectPage(page, pageInfo);
  172. AddPage(new PageReference
  173. {
  174. page = soPage,
  175. info = soPage
  176. });
  177. }
  178. }
  179. else
  180. {
  181. IMetaHubPage page;
  182. if (pageType.IsSubclassOf(typeof(ScriptableObject)))
  183. {
  184. page = (IMetaHubPage) ScriptableObject.CreateInstance(pageType);
  185. }
  186. else
  187. {
  188. page = Activator.CreateInstance(pageType) as IMetaHubPage;
  189. }
  190. if(page is IPageInfo info) AddPage(new PageReference { page = page, info = info});
  191. else AddPage(new PageReference { page = page, info = pageInfo});
  192. var method = page.GetType().GetMethod("OnEnable", BindingFlags.Default | BindingFlags.Public);
  193. method?.Invoke(page, new object[0]);
  194. }
  195. }
  196. // Sort the pages by priority then alpha
  197. foreach (var group in _pageGroupMap.Values)
  198. {
  199. group.Sort();
  200. }
  201. }
  202. private void AddPage(PageReference page)
  203. {
  204. if (string.IsNullOrEmpty(page.info.Context)) _rootPageGroup.AddPage(page);
  205. else _pageGroupMap[_contextMap[page.info.Context]].AddPage(page);
  206. }
  207. protected virtual void OnGUI()
  208. {
  209. titleContent = TitleContent;
  210. GUILayout.BeginHorizontal();
  211. GUILayout.FlexibleSpace();
  212. _searchString = EditorGUILayout.TextField(_searchString, GUI.skin.FindStyle("ToolbarSeachTextField"));
  213. GUILayout.EndHorizontal();
  214. EditorGUILayout.BeginHorizontal();
  215. DrawLeftPanel();
  216. DrawRightPanel();
  217. EditorGUILayout.EndHorizontal();
  218. }
  219. private void DrawLeftPanel()
  220. {
  221. EditorGUILayout.BeginVertical(GUILayout.Width(_leftPanelWidth));
  222. var logo = LogoImage;
  223. // Draw logo image
  224. if (logo)
  225. {
  226. float aspectRatio = logo.width / (float) logo.height;
  227. GUILayout.Box(logo, GUILayout.Width(_leftPanelWidth), GUILayout.Height(_leftPanelWidth / aspectRatio));
  228. }
  229. _leftScroll = GUILayout.BeginScrollView(_leftScroll);
  230. DrawPageGroup(_rootPageGroup);
  231. foreach (var context in _pageGroups)
  232. {
  233. DrawPageGroup(context);
  234. }
  235. GUILayout.EndScrollView();
  236. EditorGUILayout.EndVertical();
  237. }
  238. private void DrawPageGroup(PageGroup group)
  239. {
  240. if (!IsGroupVisible(group)) return;
  241. var searchMatchedGroupContext = ContextFilter.Count != 1 && IsGroupInSearch(group);
  242. List<PageReference> pages = new List<PageReference>();
  243. if (!string.IsNullOrEmpty(_searchString) && !searchMatchedGroupContext)
  244. {
  245. foreach (var page in group.Pages)
  246. {
  247. if (PageInSearch(page))
  248. {
  249. pages.Add(page);
  250. }
  251. }
  252. }
  253. if (ContextFilter.Count == 0 && (string.IsNullOrEmpty(_searchString) || pages.Count > 0))
  254. {
  255. if (null != group.Context &&
  256. (string.IsNullOrEmpty(_searchString) && group.PageCount > 0 || pages.Count > 0) &&
  257. !string.IsNullOrEmpty(group.Context.Name) && group.Context.ShowPageGroupTitle)
  258. {
  259. GUILayout.Space(8);
  260. GUILayout.Label(group.Context.Name, EditorStyles.boldLabel);
  261. }
  262. }
  263. if(!string.IsNullOrEmpty(_searchString))
  264. {
  265. for (int i = 0; i < pages.Count; i++)
  266. {
  267. DrawPageEntry(pages[i]);
  268. }
  269. }
  270. else
  271. {
  272. group.Hierarchy.Draw();
  273. }
  274. }
  275. private bool PageInSearch(PageReference page)
  276. {
  277. #if UNITY_2021_1_OR_NEWER
  278. return page.info.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
  279. #else
  280. return page.info.Name.ToLower().Contains(_searchString.ToLower());
  281. #endif
  282. }
  283. private bool IsGroupInSearch(PageGroup group)
  284. {
  285. #if UNITY_2021_1_OR_NEWER
  286. return group.Context && group.Context.Name.Contains(_searchString,
  287. StringComparison.OrdinalIgnoreCase);
  288. #else
  289. return group.Context && group.Context.Name.ToLower().Contains(_searchString.ToLower());
  290. #endif
  291. }
  292. private bool IsGroupVisible(PageGroup group)
  293. {
  294. return group.PageCount > 0 &&
  295. ContextFilter.Count == 0 && (!group.Context || group.Context.AllowWithoutContextFilter) ||
  296. ContextFilter.Contains(group.Context ? group.Context.Name : "");
  297. }
  298. private void DrawPageEntry(PageReference page)
  299. {
  300. GUIStyle optionStyle = new GUIStyle(GUI.skin.label);
  301. optionStyle.normal.background = null;
  302. optionStyle.normal.textColor = _selectedPage == page.page ? Color.white : GUI.skin.label.normal.textColor;
  303. if (null == _selectedPage)
  304. {
  305. // TODO: We will need to improve this logic.
  306. if (!string.IsNullOrEmpty(SelectedPage) && page.PageId == SelectedPage) _selectedPage = page.page;
  307. else if(string.IsNullOrEmpty(SelectedPage)) _selectedPage = page.page;
  308. }
  309. EditorGUILayout.BeginHorizontal();
  310. {
  311. Rect optionRect = GUILayoutUtility.GetRect(GUIContent.none, optionStyle, GUILayout.ExpandWidth(true), GUILayout.Height(20));
  312. bool isHover = optionRect.Contains(Event.current.mousePosition);
  313. if (isHover)
  314. {
  315. EditorGUIUtility.AddCursorRect(optionRect, MouseCursor.Link);
  316. }
  317. Color backgroundColor;
  318. if (page.page == _selectedPage)
  319. {
  320. backgroundColor = EditorGUIUtility.isProSkin ? new Color(0.22f, 0.44f, 0.88f) : new Color(0.24f, 0.48f, 0.90f);
  321. }
  322. else
  323. {
  324. backgroundColor = Color.clear;
  325. }
  326. EditorGUI.DrawRect(optionRect, backgroundColor);
  327. GUI.Label(optionRect, new GUIContent(page.info.Name, page.info.Name), optionStyle);
  328. if (Event.current.type == EventType.MouseDown && isHover)
  329. {
  330. _selectedPage = page.page;
  331. Event.current.Use();
  332. }
  333. }
  334. EditorGUILayout.EndHorizontal();
  335. }
  336. protected virtual void DrawRightPanel()
  337. {
  338. // Create a GUIStyle with a darker background color
  339. GUIStyle darkBackgroundStyle = new GUIStyle();
  340. Texture2D backgroundTexture = new Texture2D(1, 1);
  341. backgroundTexture.SetPixel(0, 0, new Color(0f, 0f, 0f, .25f));
  342. backgroundTexture.Apply();
  343. darkBackgroundStyle.normal.background = backgroundTexture;
  344. // Apply the dark background style to the right panel
  345. EditorGUILayout.BeginVertical(darkBackgroundStyle, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
  346. if (_selectedPage is ScriptableObjectPage soPage)
  347. {
  348. if(soPage.Editor is IOverrideSize size) {
  349. size.OverrideWidth = EditorGUIUtility.currentViewWidth - _leftPanelWidth;
  350. }
  351. }
  352. _selectedPage?.OnGUI();
  353. EditorGUILayout.EndVertical();
  354. }
  355. public static T ShowWindow<T>(params string[] contexts) where T : MetaHub
  356. {
  357. var window = EditorWindow.GetWindow<T>();
  358. window._selectedPage = null;
  359. window.titleContent = new GUIContent("Meta Hub");
  360. window.ContextFilter.Clear();
  361. foreach (var context in contexts)
  362. {
  363. window.ContextFilter.Add(context);
  364. }
  365. window.UpdateContextFilter();
  366. window.Show();
  367. return window;
  368. }
  369. }
  370. internal class ScriptableObjectPage : IMetaHubPage, IPageInfo
  371. {
  372. private readonly ScriptableObject _page;
  373. private string _context;
  374. private Editor _editor;
  375. private string _name;
  376. private string _prefix = "";
  377. private int _priority;
  378. public string Name => _name;
  379. public string Prefix => _prefix;
  380. public string Context => _context;
  381. public Editor Editor => _editor;
  382. public int Priority => _priority;
  383. public ScriptableObjectPage(ScriptableObject page, MetaHubPageAttribute pageInfo)
  384. {
  385. _page = page;
  386. _context = pageInfo.Context;
  387. _priority = pageInfo.Priority;
  388. _prefix = pageInfo.Prefix;
  389. UpdatePageInfo();
  390. }
  391. public ScriptableObjectPage(ScriptableObject page, string context, string prefix = "", int priority = 0)
  392. {
  393. _page = page;
  394. _context = context;
  395. _priority = priority;
  396. _prefix = prefix;
  397. UpdatePageInfo();
  398. }
  399. private void UpdatePageInfo()
  400. {
  401. if (_page is IPageInfo info)
  402. {
  403. if (!string.IsNullOrEmpty(info.Name)) _name = info.Name;
  404. if (!string.IsNullOrEmpty(info.Context)) _context = info.Context;
  405. if (!string.IsNullOrEmpty(info.Prefix)) _prefix = info.Prefix;
  406. if (info.Priority != 0) _priority = info.Priority;
  407. }
  408. else
  409. {
  410. _name = _page.name;
  411. }
  412. }
  413. public void OnGUI()
  414. {
  415. if (_page)
  416. {
  417. // Create an editor for the assigned ScriptableObject
  418. if (_editor == null || _editor.target != _page)
  419. {
  420. _editor = Editor.CreateEditor(_page);
  421. }
  422. // Render the ScriptableObject with its default editor
  423. _editor.OnInspectorGUI();
  424. }
  425. }
  426. }
  427. }