using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.SceneManagement; using UnityObject = UnityEngine.Object; namespace Unity.VisualScripting { public class VariablesPanel : ISidebarPanelContent { const string k_ObjectTabTitle = "Object Variables"; public IGraphContext context { get; } public Vector2 minSize => new Vector2(335, 200); public GUIContent titleContent { get; } public object sidebarControlHint => typeof(VariablesPanel); public VariablesPanel(IGraphContext context) { this.context = context; titleContent = new GUIContent("Blackboard", BoltCore.Icons.variablesWindow?[IconSize.Small]); tabs.Clear(); if (context?.graph is IGraphWithVariables) { tabs.Add(Graph(context.reference)); } else { tabs.Add(Graph(null)); } tabs.Add(Object(context?.reference.gameObject ?? Selection.activeGameObject)); tabs.Add(Scene()); tabs.Add(Application()); tabs.Add(Saved()); _currentTab = tabs.FirstOrDefault(t => t.enabled); _currentSubTabIdentifier = _currentTab?.currentSubTab?.identifier; PrefabUtility.prefabInstanceUpdated += OnPrefabInstanceUpdated; } void OnPrefabInstanceUpdated(GameObject instance) { if (instance == context?.reference.gameObject) { var tab = tabs.Single(t => t.header.text == k_ObjectTabTitle); OnObjectTabGUI(tab, instance); } } private readonly List tabs = new List(); private Tab _currentTab; private string _currentSubTabIdentifier; public string currentSubTabIdentifier { get => _currentSubTabIdentifier; set { _currentSubTabIdentifier = value; var _currentSubTab = tabs.Where(t => t.enabled).SelectMany(t => t.subTabs).FirstOrDefault(st => st.identifier == _currentSubTabIdentifier); if (_currentSubTab != null) { _currentTab = _currentSubTab.tab; _currentTab.currentSubTab = _currentSubTab; } } } public float GetHeight(float width) { var height = 0f; height += GetTabBarHeight(width); if (_currentTab != null) { context?.BeginEdit(false); height += _currentTab.GetHeight(width); context?.EndEdit(); } return height; } public void OnGUI(Rect position) { var y = position.y; var tabBarHeight = GetTabBarHeight(position.width); var tabButtonWidth = position.width / tabs.Count; for (var i = 0; i < tabs.Count; i++) { var tab = tabs[i]; /* * TODO: This code should be optimized at some point. * Ideally, the reinitialization of the first tab should happen once the editor enters play mode. * However, at this point, the UI was not fully reloaded yet (context.reference is in invalid state) * Maybe, an async task to monitor context.reference to the point were it was ready to use again could be a good alternative */ if (i == 0 && UnityEngine.Application.isPlaying && tab.currentSubTab != null && string.Compare(tab.currentSubTab.identifier, "Graph.Runtime", StringComparison.Ordinal) != 0) { if (context?.graph is IGraphWithVariables) { GraphPointer graphPointer = context.reference; if (graphPointer.hasData) { VariableDeclarations instanceVariables = graphPointer.GetGraphData().variables; tab.subTabs.Clear(); tab.subTabs.Add(new SubTab("Graph.Runtime", tab, VariableKind.Graph, instanceVariables, graphPointer.serializedObject, null, "Instance")); tab.MakeFirstSubTabCurrent(); } } } var tabButtonPosition = new Rect ( position.x + i * tabButtonWidth, y, tabButtonWidth, tabBarHeight ); OnTabButtonGUI(tabButtonPosition, tab); } y += tabBarHeight; y--; if (_currentTab != null) { context?.BeginEdit(); _currentTab.OnGUI(position, ref y); context?.EndEdit(); } } private float GetTabBarHeight(float width) { return Styles.tab.fixedHeight; } //TODO: remove once the asset bundle bug is fixed private void ValidateTabHeader(Tab tab) { if (tab.header.image == null) { if (tab.label.text.Trim() == "Graph") { tab.header.image = BoltCore.Icons.graphVariable[IconSize.Medium]; tab.label.image = BoltCore.Icons.graphVariable[IconSize.Small]; } else if (tab.label.text.Trim() == "Object") { tab.header.image = BoltCore.Icons.objectVariable[IconSize.Medium]; tab.label.image = BoltCore.Icons.objectVariable[IconSize.Small]; } else if (tab.label.text.Trim() == "Scene") { tab.header.image = BoltCore.Icons.sceneVariable[IconSize.Medium]; tab.label.image = BoltCore.Icons.sceneVariable[IconSize.Small]; } else if (tab.label.text.Trim() == "App") { tab.header.image = BoltCore.Icons.applicationVariable[IconSize.Medium]; tab.label.image = BoltCore.Icons.applicationVariable[IconSize.Small]; } else if (tab.label.text.Trim() == "Saved") { tab.header.image = BoltCore.Icons.savedVariable[IconSize.Medium]; tab.label.image = BoltCore.Icons.savedVariable[IconSize.Small]; } } } private void OnTabButtonGUI(Rect position, Tab tab) { ValidateTabHeader(tab); EditorGUI.BeginDisabledGroup(!tab.enabled); using (LudiqGUIUtility.iconSize.Override(IconSize.Small)) { if (GUI.Toggle(position, _currentTab == tab, tab.label, Styles.tab) && _currentTab != tab) { var subTab = tab.subTabs.FirstOrDefault(); currentSubTabIdentifier = subTab?.identifier; GUIUtility.keyboardControl = 0; GUIUtility.hotControl = 0; } } EditorGUI.EndDisabledGroup(); } private Tab Graph(GraphReference reference) { var tab = new Tab ( this, "Graph", "Graph Variables", "These variables are local to the current graph.", BoltCore.Icons.graphVariable ); if (reference != null) { if (reference.hasData) { var instanceVariables = Variables.GraphInstance(reference); tab.subTabs.Add(new SubTab("Graph.Instance", tab, VariableKind.Graph, instanceVariables, reference.serializedObject, null, "Instance")); } var definitionVariables = Variables.GraphDefinition(reference); tab.subTabs.Add(new SubTab("Graph.Definition", tab, VariableKind.Graph, definitionVariables, reference.serializedObject, null, "Definition")); } tab.MakeFirstSubTabCurrent(); return tab; } private Tab Object(GameObject @object) { var tab = new Tab ( this, "Object", k_ObjectTabTitle, "These variables are shared across the current game object.", BoltCore.Icons.objectVariable ); if (@object != null) { OnObjectTabGUI(tab, @object); } return tab; } static void OnObjectTabGUI(Tab tab, GameObject gameObject) { tab.subTabs.Clear(); if (gameObject.IsConnectedPrefabInstance()) { var instanceVariables = gameObject.GetComponent(); if (instanceVariables != null) { tab.subTabs.Add(new SubTab("Object.Instance", tab, VariableKind.Object, instanceVariables.declarations, instanceVariables, null, "Prefab Instance")); } var definition = gameObject.GetPrefabDefinition(); // Prefab could have been deleted if (definition != null) { var definitionVariables = definition.GetComponent(); if (definitionVariables != null) { tab.subTabs.Add(new SubTab("Object.Definition", tab, VariableKind.Object, definitionVariables.declarations, definitionVariables, null, "Prefab Definition")); } } } else { var variables = gameObject.GetComponent(); if (variables != null) { tab.subTabs.Add(new SubTab("Object.Instance", tab, VariableKind.Object, variables.declarations, variables, null)); } } tab.MakeFirstSubTabCurrent(); } private Tab Scene() { var tab = new Tab ( this, "Scene", "Scene Variables", "These variables are shared across the current scene.", BoltCore.Icons.sceneVariable ); for (int i = 0; i < SceneManager.sceneCount; i++) { var scene = SceneManager.GetSceneAt(i); // Trying to fetch the singleton while the scene isn't completely // loaded seems to mess with the instantiation or handles and create duplicates if (!scene.isLoaded) { continue; } if (BoltCore.Configuration.createSceneVariables || SceneVariables.InstantiatedIn(scene)) { var sceneVariables = SceneVariables.Instance(scene); var declarations = sceneVariables.GetComponent().declarations; var owner = sceneVariables; var title = StringUtility.FallbackWhitespace(sceneVariables.gameObject.scene.name, "Untitled"); declarations.Kind = VariableKind.Scene; tab.subTabs.Add(new SubTab("Scene", tab, VariableKind.Scene, declarations, owner, null, title)); } } tab.MakeFirstSubTabCurrent(); return tab; } private Tab Application() { void SaveAppVariablesAsset() => EditorVariablesUtility.SaveVariableAsset(ApplicationVariables.asset, ApplicationVariables.assetPath); if (EditorApplication.isPlaying) { var tab = new Tab ( this, "App", "Application Variables", "These variables are shared across scenes. They will be reset once you exit playmode.", BoltCore.Icons.applicationVariable ); if (ApplicationVariables.runtime != null) { tab.subTabs.Add(new SubTab("Application", tab, VariableKind.Application, ApplicationVariables.runtime, null, SaveAppVariablesAsset)); } tab.MakeFirstSubTabCurrent(); return tab; } else { var tab = new Tab ( this, "App", "Application Variables", "These variables are shared across scenes. They will be reset once the application quits.", BoltCore.Icons.applicationVariable ); if (ApplicationVariables.asset?.declarations != null) { tab.subTabs.Add(new SubTab("Application", tab, VariableKind.Application, ApplicationVariables.asset.declarations, ApplicationVariables.asset, SaveAppVariablesAsset)); } tab.MakeFirstSubTabCurrent(); return tab; } } private Tab Saved() { void SaveSavedVariableAsset() => EditorVariablesUtility.SaveVariableAsset(SavedVariables.asset, SavedVariables.assetPath); if (EditorApplication.isPlaying) { var tab = new Tab ( this, "Saved", "Saved Variables", "These variables will persist even after the application quits. Unity object references are not supported.", BoltCore.Icons.savedVariable ); if (SavedVariables.merged != null) { tab.subTabs.Add(new SubTab("Saved.Merged", tab, VariableKind.Saved, SavedVariables.merged, null, SaveSavedVariableAsset, "Merged", "The currently merged variables.")); } tab.MakeFirstSubTabCurrent(); return tab; } else { var tab = new Tab ( this, "Saved", "Saved Variables", "These variables will persist even after the application quits. Unity object references are not supported.", BoltCore.Icons.savedVariable ); if (SavedVariables.asset?.declarations != null) { tab.subTabs.Add(new SubTab("Saved.Initial", tab, VariableKind.Saved, SavedVariables.asset.declarations, SavedVariables.asset, SaveSavedVariableAsset, "Initial", "Default variables for new games.")); } if (SavedVariables.saved != null) { tab.subTabs.Add(new SubTab("Saved.Saved", tab, VariableKind.Saved, SavedVariables.saved, null, SaveSavedVariableAsset, "Saved", "The currently saved variables.")); } tab.MakeFirstSubTabCurrent(); return tab; } } public static class Styles { static Styles() { tab = new GUIStyle(EditorStyles.toolbarButton); tab.margin = new RectOffset(0, 0, 0, 0); tab.padding = new RectOffset(0, 0, 0, 0); tab.fixedHeight = 22; subTab = new GUIStyle(tab); } public static readonly GUIStyle tab; public static readonly GUIStyle subTab; } private class Tab { public Tab(VariablesPanel panel, string shortTitle, string title, string description, EditorTexture icon, params SubTab[] subTabs) { Ensure.That(nameof(panel)).IsNotNull(panel); Ensure.That(nameof(shortTitle)).IsNotNull(shortTitle); Ensure.That(nameof(title)).IsNotNull(title); Ensure.That(nameof(description)).IsNotNull(description); Ensure.That(nameof(subTabs)).IsNotNull(subTabs); this.panel = panel; header = new GUIContent(title, icon?[IconSize.Medium], description); label = new GUIContent(" " + shortTitle, icon?[IconSize.Small]); this.subTabs = new List(subTabs.NotNull()); currentSubTab = this.subTabs.FirstOrDefault(); } public readonly VariablesPanel panel; public readonly List subTabs; public SubTab currentSubTab { get; set; } public GUIContent label { get; } public GUIContent header { get; } public bool enabled => subTabs.Count > 0; public float GetHeight(float width) { var height = 0f; if (BoltCore.Configuration.showVariablesHelp) { height += LudiqGUI.GetHeaderHeight(header, width, false); height--; } if (subTabs.Count > 1) { height += GetSubTabBarHeight(width); } if (enabled && currentSubTab != null) { height += currentSubTab.GetHeight(width); } return height; } public void OnGUI(Rect position, ref float y) { if (BoltCore.Configuration.showVariablesHelp) { EditorGUI.BeginDisabledGroup(!enabled); LudiqGUI.OnHeaderGUI(header, position, ref y, false); EditorGUI.EndDisabledGroup(); } if (subTabs.Count > 1) { var subTabBarHeight = GetSubTabBarHeight(position.width); var subTabButtonWidth = position.width / subTabs.Count; for (var i = 0; i < subTabs.Count; i++) { var subTab = subTabs[i]; var subTabButtonPosition = new Rect ( position.x + i * subTabButtonWidth, y, subTabButtonWidth, subTabBarHeight ); OnSubTabButtonGUI(subTabButtonPosition, subTab); } y += subTabBarHeight; } if (BoltCore.Configuration.showVariablesHelp || subTabs.Count > 1) { y--; } if (enabled) { currentSubTab?.OnGUI(position, ref y); } } private float GetSubTabBarHeight(float width) { return Styles.subTab.fixedHeight; } private void OnSubTabButtonGUI(Rect position, SubTab subTab) { if (GUI.Toggle(position, currentSubTab == subTab, subTab.label, Styles.subTab) && currentSubTab != subTab) { panel.currentSubTabIdentifier = subTab.identifier; GUIUtility.keyboardControl = 0; GUIUtility.hotControl = 0; } } public void MakeFirstSubTabCurrent() { currentSubTab = subTabs.FirstOrDefault(); } } // Implementation note: We're being extra careful with null checks when instantiating // subtabs because the variable declarations have wildly different, hard to predict lifetimes. // This way, the tab will just show up as disabled in the first frame if the declarations // aren't yet deserialized/fetched/merged/etc. private class SubTab { public SubTab(string identifier, Tab tab, VariableKind kind, VariableDeclarations declarations, UnityObject targetObject, Action save, string label = "Default", string tooltip = null) { Ensure.That(nameof(tab)).IsNotNull(tab); Ensure.That(nameof(declarations)).IsNotNull(declarations); Ensure.That(nameof(label)).IsNotNull(label); this.tab = tab; this.label = new GUIContent(label, tooltip); this.targetObject = targetObject; this.identifier = identifier; metadata = Metadata.Root().StaticObject(declarations); inspector = metadata.Inspector(); inspector.kind = kind; this.save = save; } public string identifier { get; } public Tab tab { get; } private readonly Metadata metadata; private readonly VariableDeclarationsInspector inspector; private readonly UnityObject targetObject; private readonly Action save; public GUIContent label { get; } public float GetHeight(float width) { using (LudiqEditorUtility.editedObject.Override(targetObject)) { return GetDeclarationsHeight(width); } } public void OnGUI(Rect position, ref float y) { using (LudiqEditorUtility.editedObject.Override(targetObject)) { EditorGUI.BeginChangeCheck(); inspector.Draw(position.VerticalSection(ref y, GetDeclarationsHeight(position.width)), GUIContent.none); if (EditorGUI.EndChangeCheck()) { save?.Invoke(); } } } private float GetDeclarationsHeight(float width) { return inspector.GetCachedHeight(width, GUIContent.none, null); } } } }