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<Tab> tabs = new List<Tab>();
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<IGraphDataWithVariables>().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<Variables>();
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<Variables>();
if (definitionVariables != null)
{
tab.subTabs.Add(new SubTab("Object.Definition", tab, VariableKind.Object, definitionVariables.declarations, definitionVariables, null, "Prefab Definition"));
}
}
}
else
{
var variables = gameObject.GetComponent<Variables>();
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<Variables>().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<SubTab>(subTabs.NotNull());
currentSubTab = this.subTabs.FirstOrDefault();
}
public readonly VariablesPanel panel;
public readonly List<SubTab> 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<VariableDeclarationsInspector>();
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);
}
}
}
}