A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEditor.ShaderGraph.Drawing;
4using UnityEditor.ShaderGraph.Drawing.Colors;
5using UnityEditor.UIElements;
6using UnityEngine;
7using UnityEngine.UIElements;
8
9namespace UnityEditor.ShaderGraph
10{
11 [CustomEditor(typeof(ShaderGraphHeatmapValues))]
12 class ShaderGraphHeatmapValuesEditor : Editor
13 {
14 const string k_TemplatePath = "Packages/com.unity.shadergraph/Editor/Resources/UXML/HeatmapValuesEditor.uxml";
15 const string k_StylePath = "Packages/com.unity.shadergraph/Editor/Resources/Styles/HeatmapValuesEditor.uss";
16
17 const string k_ColorsListName = "colors-list";
18 const string k_NodeListName = "nodes-list";
19 const string k_SubgraphListName = "subgraph-list";
20 const string k_NodeTitleColumnName = "node";
21 const string k_SubgraphColumnName = "subgraph";
22 const string k_HeatValueColumnName = "value";
23
24 const string k_HeatFieldUssClassName = "sg-heatmap__heat-field";
25 const string k_SubgraphPickerUssClassName = "sg-heatmap__subgraph-picker";
26 const string k_NodeLabelUssClassName = "sg-heatmap__node-label";
27
28 const string k_RefreshNodesHintName = "refresh-nodes-hint";
29 const string k_RefreshNodesButtonName = "refresh-nodes-button";
30 const string k_HelpBoxHiddenUssModifier = "sg-heatmap__help-box--hidden";
31
32 ShaderGraphHeatmapValues HeatmapValuesTarget => target as ShaderGraphHeatmapValues;
33
34 MultiColumnListView m_NodesListView;
35 MultiColumnListView m_SubgraphListView;
36 VisualElement m_RefreshNodesHint;
37
38 internal static void UpdateShaderGraphWindows()
39 {
40 foreach (var window in Resources.FindObjectsOfTypeAll<MaterialGraphEditWindow>())
41 {
42 var graphEditorView = window.graphEditorView;
43 if (graphEditorView == null)
44 {
45 continue;
46 }
47
48 var colorManager = graphEditorView.colorManager;
49 if (colorManager.activeProviderName != HeatmapColors.Title)
50 {
51 continue;
52 }
53
54 var nodeList = graphEditorView.Query<MaterialNodeView>().ToList();
55 colorManager.UpdateNodeViews(nodeList);
56 }
57 }
58
59 static Dictionary<string, string> s_NodeTypeNamesToTitles;
60 static Dictionary<string, string> NodeTypeNamesToTitles
61 {
62 get
63 {
64 if (s_NodeTypeNamesToTitles is null)
65 {
66 s_NodeTypeNamesToTitles = new Dictionary<string, string>();
67 foreach (var knownNodeType in NodeClassCache.knownNodeTypes)
68 {
69 var node = (AbstractMaterialNode) Activator.CreateInstance(knownNodeType);
70 s_NodeTypeNamesToTitles[knownNodeType.Name] = node.name;
71 }
72 }
73
74 return s_NodeTypeNamesToTitles;
75 }
76 }
77
78 static string GetTitleForNode(string nodeTypeName)
79 {
80 return string.IsNullOrEmpty(nodeTypeName)
81 ? string.Empty
82 : NodeTypeNamesToTitles.GetValueOrDefault(nodeTypeName, nodeTypeName);
83 }
84
85 static void ConfigureNodeLabelColumn(Column column, HeatmapEntries entries)
86 {
87 column.makeCell = () =>
88 {
89 var ve = new Label();
90 ve.AddToClassList(k_NodeLabelUssClassName);
91 return ve;
92 };
93 column.bindCell = (v, i) =>
94 {
95 var label = (Label) v;
96 label.text = GetTitleForNode(entries.Entries[i].m_NodeName);
97 };
98 }
99
100 void ConfigureSubgraphPickerColumn(Column column)
101 {
102 var serializedEntries = serializedObject.FindProperty("m_Subgraphs");
103 var entries = HeatmapValuesTarget.Subgraphs;
104
105 column.makeCell = () =>
106 {
107 var ve = new ObjectField
108 {
109 tooltip = "Subgraph asset.",
110 objectType = typeof(SubGraphAsset),
111 allowSceneObjects = false,
112 };
113
114 ve.AddToClassList(k_SubgraphPickerUssClassName);
115 return ve;
116 };
117
118 column.bindCell = (v, i) =>
119 {
120 var objectField = (ObjectField) v;
121 objectField.RegisterCallback<ChangeEvent<UnityEngine.Object>, int>(OnSubgraphChanged, i);
122
123 if (string.IsNullOrEmpty(entries.Entries[i].m_NodeName))
124 {
125 return;
126 }
127
128 var assetPath = AssetDatabase.GUIDToAssetPath(entries.Entries[i].m_NodeName);
129 var asset = AssetDatabase.LoadAssetAtPath<SubGraphAsset>(assetPath);
130 objectField.SetValueWithoutNotify(asset);
131 };
132
133 column.unbindCell = (v, i) =>
134 {
135 var objectField = (ObjectField) v;
136 objectField.UnregisterCallback<ChangeEvent<UnityEngine.Object>, int>(OnSubgraphChanged);
137 };
138
139 return;
140
141 void OnSubgraphChanged(ChangeEvent<UnityEngine.Object> changeEvent, int index)
142 {
143 if (changeEvent.newValue is not SubGraphAsset subgraph)
144 {
145 return;
146 }
147
148 serializedEntries
149 .FindPropertyRelative("m_Entries")
150 .GetArrayElementAtIndex(index)
151 .FindPropertyRelative("m_NodeName")
152 .stringValue = subgraph.assetGuid;
153
154 ApplyChanges();
155 }
156 }
157
158 void ConfigureCategoryColumn(Column c, SerializedProperty serializedEntries, HeatmapEntries entries)
159 {
160 c.makeCell = () =>
161 {
162 var ve = new IntegerField
163 {
164 tooltip = "Category assigned to this node, which determines its color.",
165 isDelayed = true,
166 };
167
168 ve.AddToClassList(k_HeatFieldUssClassName);
169 return ve;
170 };
171 c.bindCell = (v, i) =>
172 {
173 var intField = (IntegerField) v;
174 intField.SetValueWithoutNotify(entries.Entries[i].m_Category);
175 intField.RegisterCallback<ChangeEvent<int>, int>(OnHeatChanged, i);
176 };
177 c.unbindCell = (v, i) =>
178 {
179 var intField = (IntegerField) v;
180 intField.UnregisterCallback<ChangeEvent<int>, int>(OnHeatChanged);
181 };
182
183 return;
184
185 void OnHeatChanged(ChangeEvent<int> changeEvent, int index)
186 {
187 serializedEntries
188 .FindPropertyRelative("m_Entries")
189 .GetArrayElementAtIndex(index)
190 .FindPropertyRelative("m_Category")
191 .intValue = changeEvent.newValue;
192
193 ApplyChanges();
194 }
195 }
196
197 void OnEnable()
198 {
199 Undo.undoRedoPerformed += OnUndoRedo;
200 }
201
202 void OnDisable()
203 {
204 Undo.undoRedoPerformed -= OnUndoRedo;
205 }
206
207 void OnUndoRedo()
208 {
209 // Target might not be what we expect if our inspector isn't focused.
210 if (HeatmapValuesTarget != null)
211 {
212 m_RefreshNodesHint.EnableInClassList(k_HelpBoxHiddenUssModifier, HeatmapValuesTarget.ContainsAllApplicableNodes());
213 }
214
215 UpdateShaderGraphWindows();
216 }
217
218 void RefreshNodeListFromProject()
219 {
220 Undo.RecordObject(target, $"Refresh Node List in {target.name}");
221
222 HeatmapValuesTarget.PopulateNodesFromProject();
223 serializedObject.Update();
224
225 m_NodesListView.RefreshItems();
226 m_RefreshNodesHint.EnableInClassList(k_HelpBoxHiddenUssModifier, true);
227
228 UpdateShaderGraphWindows();
229 }
230
231 void ApplyChanges()
232 {
233 serializedObject.ApplyModifiedProperties();
234 UpdateShaderGraphWindows();
235 }
236
237 public override VisualElement CreateInspectorGUI()
238 {
239 var root = new VisualElement();
240 AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(k_TemplatePath).CloneTree(root);
241
242 var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(k_StylePath);
243 root.styleSheets.Add(styleSheet);
244
245 var colorsListView = root.Q<ListView>(k_ColorsListName);
246 colorsListView.bindingPath = serializedObject.FindProperty(nameof(ShaderGraphHeatmapValues.m_Colors)).propertyPath;
247 colorsListView.makeItem = () => new ColorField {showAlpha = false, hdr = false};
248 colorsListView.bindItem = (v, i) =>
249 {
250 var colorField = (ColorField) v;
251 colorField.label = $"Category {i}";
252 colorField.tooltip = $"Color for category {i}.";
253 colorField.RegisterValueChangedCallback(_ => ApplyChanges());
254
255 // Assigning bindingPath makes itemsSource into a list of SerializedProperties, so we can bind directly.
256 colorField.BindProperty((SerializedProperty) colorsListView.itemsSource[i]);
257 };
258
259 colorsListView.Bind(serializedObject);
260
261 var nodeEntries = HeatmapValuesTarget.Nodes;
262 m_NodesListView = root.Q<MultiColumnListView>(k_NodeListName);
263
264 m_RefreshNodesHint = root.Q<HelpBox>(k_RefreshNodesHintName);
265 m_RefreshNodesHint.EnableInClassList(k_HelpBoxHiddenUssModifier, HeatmapValuesTarget.ContainsAllApplicableNodes());
266 m_RefreshNodesHint.Q<Button>(k_RefreshNodesButtonName).clicked += RefreshNodeListFromProject;
267
268 var nodesSerializedProperty = serializedObject.FindProperty("m_Nodes");
269 ConfigureNodeLabelColumn(m_NodesListView.columns[k_NodeTitleColumnName], nodeEntries);
270 ConfigureCategoryColumn(m_NodesListView.columns[k_HeatValueColumnName], nodesSerializedProperty, nodeEntries);
271
272 m_NodesListView.AddManipulator(new ContextualMenuManipulator(evt =>
273 {
274 evt.menu.AppendSeparator();
275 evt.menu.AppendAction("Refresh Node List", _ => RefreshNodeListFromProject());
276 }));
277
278 // MultiColumnListView doesn't support binding as of writing, but we can still track property changes
279 // to reflect undo, redo, reset, etc.
280 m_NodesListView.TrackPropertyValue(nodesSerializedProperty, _ => { m_NodesListView.RefreshItems(); });
281 m_NodesListView.itemsSource = nodeEntries.Entries;
282
283 var subgraphEntries = HeatmapValuesTarget.Subgraphs;
284 m_SubgraphListView = root.Q<MultiColumnListView>(k_SubgraphListName);
285
286 var subgraphsSerializedProperty = serializedObject.FindProperty("m_Subgraphs");
287 ConfigureSubgraphPickerColumn(m_SubgraphListView.columns[k_SubgraphColumnName]);
288 ConfigureCategoryColumn(m_SubgraphListView.columns[k_HeatValueColumnName], subgraphsSerializedProperty, subgraphEntries);
289
290 m_SubgraphListView.TrackPropertyValue(subgraphsSerializedProperty, _ => { m_SubgraphListView.RefreshItems(); });
291 m_SubgraphListView.itemsSource = subgraphEntries.Entries;
292
293 return root;
294 }
295 }
296}