You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1311 lines
53 KiB
1311 lines
53 KiB
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Text; |
|
using UnityEngine; |
|
using UnityEditor.Graphing; |
|
using UnityEditor.Graphing.Util; |
|
using Debug = UnityEngine.Debug; |
|
using Object = UnityEngine.Object; |
|
using UnityEngine.Rendering; |
|
|
|
using UnityEditor.UIElements; |
|
using Edge = UnityEditor.Experimental.GraphView.Edge; |
|
using UnityEditor.Experimental.GraphView; |
|
using UnityEditor.ShaderGraph.Internal; |
|
using UnityEditor.ShaderGraph.Serialization; |
|
using UnityEngine.UIElements; |
|
using UnityEditor.VersionControl; |
|
|
|
using Unity.Profiling; |
|
using UnityEngine.Assertions; |
|
|
|
namespace UnityEditor.ShaderGraph.Drawing |
|
{ |
|
class MaterialGraphEditWindow : EditorWindow |
|
{ |
|
// For conversion to Sub Graph: keys for remembering the user's desired path |
|
const string k_PrevSubGraphPathKey = "SHADER_GRAPH_CONVERT_TO_SUB_GRAPH_PATH"; |
|
const string k_PrevSubGraphPathDefaultValue = "?"; // Special character that NTFS does not allow, so that no directory could match it. |
|
|
|
[SerializeField] |
|
string m_Selected; |
|
|
|
[SerializeField] |
|
GraphObject m_GraphObject; |
|
|
|
// this stores the contents of the file on disk, as of the last time we saved or loaded it from disk |
|
[SerializeField] |
|
string m_LastSerializedFileContents; |
|
|
|
[NonSerialized] |
|
HashSet<string> m_ChangedFileDependencyGUIDs = new HashSet<string>(); |
|
|
|
ColorSpace m_ColorSpace; |
|
RenderPipelineAsset m_RenderPipelineAsset; |
|
|
|
[NonSerialized] |
|
bool m_FrameAllAfterLayout; |
|
[NonSerialized] |
|
bool m_HasError; |
|
[NonSerialized] |
|
bool m_ProTheme; |
|
[NonSerialized] |
|
int m_customInterpWarn; |
|
[NonSerialized] |
|
int m_customInterpErr; |
|
|
|
[SerializeField] |
|
bool m_AssetMaybeChangedOnDisk; |
|
|
|
[SerializeField] |
|
bool m_AssetMaybeDeleted; |
|
|
|
MessageManager m_MessageManager; |
|
MessageManager messageManager |
|
{ |
|
get { return m_MessageManager ?? (m_MessageManager = new MessageManager()); } |
|
} |
|
|
|
GraphEditorView m_GraphEditorView; |
|
internal GraphEditorView graphEditorView |
|
{ |
|
get { return m_GraphEditorView; } |
|
private set |
|
{ |
|
if (m_GraphEditorView != null) |
|
{ |
|
m_GraphEditorView.RemoveFromHierarchy(); |
|
m_GraphEditorView.Dispose(); |
|
} |
|
|
|
m_GraphEditorView = value; |
|
|
|
if (m_GraphEditorView != null) |
|
{ |
|
m_GraphEditorView.saveRequested += () => SaveAsset(); |
|
m_GraphEditorView.saveAsRequested += SaveAs; |
|
m_GraphEditorView.convertToSubgraphRequested += ToSubGraph; |
|
m_GraphEditorView.showInProjectRequested += PingAsset; |
|
m_GraphEditorView.isCheckedOut += IsGraphAssetCheckedOut; |
|
m_GraphEditorView.checkOut += CheckoutAsset; |
|
m_GraphEditorView.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged); |
|
m_FrameAllAfterLayout = true; |
|
this.rootVisualElement.Add(graphEditorView); |
|
} |
|
} |
|
} |
|
|
|
internal GraphObject graphObject |
|
{ |
|
get { return m_GraphObject; } |
|
set |
|
{ |
|
if (m_GraphObject != null) |
|
DestroyImmediate(m_GraphObject); |
|
m_GraphObject = value; |
|
} |
|
} |
|
|
|
public string selectedGuid |
|
{ |
|
get { return m_Selected; } |
|
private set |
|
{ |
|
m_Selected = value; |
|
} |
|
} |
|
|
|
public string assetName |
|
{ |
|
get { return titleContent.text; } |
|
} |
|
|
|
bool AssetFileExists() |
|
{ |
|
var assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
return File.Exists(assetPath); |
|
} |
|
|
|
// returns true when the graph has been successfully saved, or the user has indicated they are ok with discarding the local graph |
|
// returns false when saving has failed |
|
bool DisplayDeletedFromDiskDialog(bool reopen = true) |
|
{ |
|
// first double check if we've actually been deleted |
|
bool saved = false; |
|
bool okToClose = false; |
|
string originalAssetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
|
|
while (true) |
|
{ |
|
int option = EditorUtility.DisplayDialogComplex( |
|
"Graph removed from project", |
|
"The file has been deleted or removed from the project folder.\n\n" + |
|
originalAssetPath + |
|
"\n\nWould you like to save your Graph Asset?", |
|
"Save As...", "Cancel", "Discard Graph and Close Window"); |
|
|
|
if (option == 0) |
|
{ |
|
string savedPath = SaveAsImplementation(false); |
|
if (savedPath != null) |
|
{ |
|
saved = true; |
|
|
|
// either close or reopen the local window editor |
|
graphObject = null; |
|
selectedGuid = (reopen ? AssetDatabase.AssetPathToGUID(savedPath) : null); |
|
|
|
break; |
|
} |
|
} |
|
else if (option == 2) |
|
{ |
|
okToClose = true; |
|
graphObject = null; |
|
selectedGuid = null; |
|
break; |
|
} |
|
else if (option == 1) |
|
{ |
|
// continue in deleted state... |
|
break; |
|
} |
|
} |
|
|
|
return (saved || okToClose); |
|
} |
|
|
|
void Update() |
|
{ |
|
if (m_HasError) |
|
return; |
|
|
|
bool updateTitle = false; |
|
|
|
if (m_AssetMaybeDeleted) |
|
{ |
|
m_AssetMaybeDeleted = false; |
|
if (AssetFileExists()) |
|
{ |
|
// it exists... just to be sure, let's check if it changed |
|
m_AssetMaybeChangedOnDisk = true; |
|
} |
|
else |
|
{ |
|
// it was really deleted, ask the user what to do |
|
bool handled = DisplayDeletedFromDiskDialog(true); |
|
} |
|
updateTitle = true; |
|
} |
|
|
|
if (PlayerSettings.colorSpace != m_ColorSpace) |
|
{ |
|
graphEditorView = null; |
|
m_ColorSpace = PlayerSettings.colorSpace; |
|
} |
|
|
|
if (GraphicsSettings.renderPipelineAsset != m_RenderPipelineAsset) |
|
{ |
|
graphEditorView = null; |
|
m_RenderPipelineAsset = GraphicsSettings.renderPipelineAsset; |
|
} |
|
|
|
if (EditorGUIUtility.isProSkin != m_ProTheme) |
|
{ |
|
if (graphObject != null && graphObject.graph != null) |
|
{ |
|
updateTitle = true; // trigger icon swap |
|
m_ProTheme = EditorGUIUtility.isProSkin; |
|
} |
|
} |
|
|
|
bool revalidate = false; |
|
if (m_customInterpWarn != ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold) |
|
{ |
|
m_customInterpWarn = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold; |
|
revalidate = true; |
|
} |
|
if (m_customInterpErr != ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold) |
|
{ |
|
m_customInterpErr = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold; |
|
revalidate = true; |
|
} |
|
if (revalidate) |
|
{ |
|
graphEditorView?.graphView?.graph?.ValidateGraph(); |
|
} |
|
|
|
if (m_AssetMaybeChangedOnDisk) |
|
{ |
|
m_AssetMaybeChangedOnDisk = false; |
|
|
|
// if we don't have any graph, then it doesn't really matter if the file on disk changed or not |
|
// as we're going to reload it below anyways |
|
if (graphObject?.graph != null) |
|
{ |
|
// check if it actually did change on disk |
|
if (FileOnDiskHasChanged()) |
|
{ |
|
// don't worry people about "losing changes" unless there are changes to lose |
|
bool graphChanged = GraphHasChangedSinceLastSerialization(); |
|
|
|
if (EditorUtility.DisplayDialog( |
|
"Graph has changed on disk", |
|
AssetDatabase.GUIDToAssetPath(selectedGuid) + "\n\n" + |
|
(graphChanged ? "Do you want to reload it and lose the changes made in the graph?" : "Do you want to reload it?"), |
|
graphChanged ? "Discard Changes And Reload" : "Reload", |
|
"Don't Reload")) |
|
{ |
|
// clear graph, trigger reload |
|
graphObject = null; |
|
} |
|
} |
|
} |
|
updateTitle = true; |
|
} |
|
|
|
try |
|
{ |
|
if (graphObject == null && selectedGuid != null) |
|
{ |
|
var guid = selectedGuid; |
|
selectedGuid = null; |
|
Initialize(guid); |
|
} |
|
|
|
if (graphObject == null) |
|
{ |
|
Close(); |
|
return; |
|
} |
|
|
|
var materialGraph = graphObject.graph as GraphData; |
|
if (materialGraph == null) |
|
return; |
|
|
|
if (graphEditorView == null) |
|
{ |
|
messageManager.ClearAll(); |
|
materialGraph.messageManager = messageManager; |
|
string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
string graphName = Path.GetFileNameWithoutExtension(assetPath); |
|
|
|
graphEditorView = new GraphEditorView(this, materialGraph, messageManager, graphName) |
|
{ |
|
viewDataKey = selectedGuid, |
|
}; |
|
m_ColorSpace = PlayerSettings.colorSpace; |
|
m_RenderPipelineAsset = GraphicsSettings.renderPipelineAsset; |
|
graphObject.Validate(); |
|
|
|
// update blackboard title for the new graphEditorView |
|
updateTitle = true; |
|
} |
|
|
|
if (m_ChangedFileDependencyGUIDs.Count > 0 && graphObject != null && graphObject.graph != null) |
|
{ |
|
bool reloadedSomething = false; |
|
var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>(); |
|
foreach (var subGraphNode in subGraphNodes) |
|
{ |
|
var reloaded = subGraphNode.Reload(m_ChangedFileDependencyGUIDs); |
|
reloadedSomething |= reloaded; |
|
} |
|
if (subGraphNodes.Count() > 0) |
|
{ |
|
// Keywords always need to be updated to test against variant limit |
|
// No Keywords may indicate removal and this may have now made the Graph valid again |
|
// Need to validate Graph to clear errors in this case |
|
materialGraph.OnKeywordChanged(); |
|
|
|
UpdateDropdownEntries(); |
|
materialGraph.OnDropdownChanged(); |
|
} |
|
foreach (var customFunctionNode in graphObject.graph.GetNodes<CustomFunctionNode>()) |
|
{ |
|
var reloaded = customFunctionNode.Reload(m_ChangedFileDependencyGUIDs); |
|
reloadedSomething |= reloaded; |
|
} |
|
|
|
// reloading files may change serialization |
|
if (reloadedSomething) |
|
{ |
|
updateTitle = true; |
|
|
|
// may also need to re-run validation/concretization |
|
graphObject.Validate(); |
|
} |
|
|
|
m_ChangedFileDependencyGUIDs.Clear(); |
|
} |
|
|
|
var wasUndoRedoPerformed = graphObject.wasUndoRedoPerformed; |
|
|
|
if (wasUndoRedoPerformed) |
|
{ |
|
graphEditorView.HandleGraphChanges(true); |
|
graphObject.graph.ClearChanges(); |
|
graphObject.HandleUndoRedo(); |
|
} |
|
|
|
if (graphObject.isDirty || wasUndoRedoPerformed) |
|
{ |
|
updateTitle = true; |
|
graphObject.isDirty = false; |
|
hasUnsavedChanges = false; |
|
} |
|
|
|
// Called again to handle changes from deserialization in case an undo/redo was performed |
|
graphEditorView.HandleGraphChanges(wasUndoRedoPerformed); |
|
graphObject.graph.ClearChanges(); |
|
|
|
if (updateTitle) |
|
UpdateTitle(); |
|
} |
|
catch (Exception e) |
|
{ |
|
m_HasError = true; |
|
m_GraphEditorView = null; |
|
graphObject = null; |
|
Debug.LogException(e); |
|
throw; |
|
} |
|
} |
|
|
|
public void ReloadSubGraphsOnNextUpdate(List<string> changedFileGUIDs) |
|
{ |
|
foreach (var changedFileGUID in changedFileGUIDs) |
|
{ |
|
m_ChangedFileDependencyGUIDs.Add(changedFileGUID); |
|
} |
|
} |
|
|
|
void UpdateDropdownEntries() |
|
{ |
|
var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>(); |
|
foreach (var subGraphNode in subGraphNodes) |
|
{ |
|
var nodeView = graphEditorView.graphView.nodes.ToList().OfType<IShaderNodeView>() |
|
.FirstOrDefault(p => p.node != null && p.node == subGraphNode); |
|
if (nodeView != null) |
|
{ |
|
nodeView.UpdateDropdownEntries(); |
|
} |
|
} |
|
} |
|
|
|
void OnEnable() |
|
{ |
|
this.SetAntiAliasing(4); |
|
} |
|
|
|
void OnDisable() |
|
{ |
|
graphEditorView = null; |
|
messageManager.ClearAll(); |
|
} |
|
|
|
// returns true only when the file on disk doesn't match the graph we last loaded or saved to disk (i.e. someone else changed it) |
|
internal bool FileOnDiskHasChanged() |
|
{ |
|
var currentFileJson = ReadAssetFile(); |
|
return !string.Equals(currentFileJson, m_LastSerializedFileContents, StringComparison.Ordinal); |
|
} |
|
|
|
// returns true only when the graph in this window would serialize different from the last time we loaded or saved it |
|
internal bool GraphHasChangedSinceLastSerialization() |
|
{ |
|
Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code |
|
var currentGraphJson = MultiJson.Serialize(graphObject.graph); |
|
return !string.Equals(currentGraphJson, m_LastSerializedFileContents, StringComparison.Ordinal); |
|
} |
|
|
|
// returns true only when saving the graph in this window would serialize different from the file on disk |
|
internal bool GraphIsDifferentFromFileOnDisk() |
|
{ |
|
Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code |
|
var currentGraphJson = MultiJson.Serialize(graphObject.graph); |
|
var currentFileJson = ReadAssetFile(); |
|
return !string.Equals(currentGraphJson, currentFileJson, StringComparison.Ordinal); |
|
} |
|
|
|
// NOTE: we're using the AssetPostprocessor Asset Import and Deleted callbacks as a proxy for detecting file changes |
|
// We could probably replace both m_AssetMaybeDeleted and m_AssetMaybeChangedOnDisk with a combined "need to check the real status of the file" flag |
|
public void CheckForChanges() |
|
{ |
|
if (!m_AssetMaybeDeleted && graphObject?.graph != null) |
|
{ |
|
m_AssetMaybeChangedOnDisk = true; |
|
UpdateTitle(); |
|
} |
|
} |
|
|
|
public void AssetWasDeleted() |
|
{ |
|
m_AssetMaybeDeleted = true; |
|
UpdateTitle(); |
|
} |
|
|
|
public void UpdateTitle() |
|
{ |
|
string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
string shaderName = Path.GetFileNameWithoutExtension(assetPath); |
|
|
|
// update blackboard title (before we add suffixes) |
|
if (graphEditorView != null) |
|
graphEditorView.assetName = shaderName; |
|
|
|
// build the window title (with suffixes) |
|
string title = shaderName; |
|
if (graphObject?.graph == null) |
|
title = title + " (nothing loaded)"; |
|
else |
|
{ |
|
if (GraphHasChangedSinceLastSerialization()) |
|
{ |
|
hasUnsavedChanges = true; |
|
// This is the message EditorWindow will show when prompting to close while dirty |
|
saveChangesMessage = GetSaveChangesMessage(); |
|
} |
|
else |
|
{ |
|
hasUnsavedChanges = false; |
|
saveChangesMessage = ""; |
|
} |
|
if (!AssetFileExists()) |
|
title = title + " (deleted)"; |
|
} |
|
|
|
// get window icon |
|
bool isSubGraph = graphObject?.graph?.isSubGraph ?? false; |
|
Texture2D icon; |
|
{ |
|
string theme = EditorGUIUtility.isProSkin ? "_dark" : "_light"; |
|
if (isSubGraph) |
|
icon = Resources.Load<Texture2D>("Icons/sg_subgraph_icon_gray" + theme); |
|
else |
|
icon = Resources.Load<Texture2D>("Icons/sg_graph_icon_gray" + theme); |
|
} |
|
|
|
titleContent = new GUIContent(title, icon); |
|
} |
|
|
|
void OnDestroy() |
|
{ |
|
// Prompting the user if they want to close is mostly handled via the EditorWindow's system (hasUnsavedChanges). |
|
// There's unfortunately a code path (Reload Window) that doesn't go through this path. The old logic is left |
|
// here as a fallback to catch this. This has one edge case with "Reload Window" -> "Cancel" which will produce |
|
// two shader graph windows: one unmodified (that the editor opens) and one modified (that we open below). |
|
|
|
// we are closing the shadergraph window |
|
MaterialGraphEditWindow newWindow = null; |
|
if (!PromptSaveIfDirtyOnQuit()) |
|
{ |
|
// user does not want to close the window. |
|
// we can't stop the close from this code path though.. |
|
// all we can do is open a new window and transfer our data to the new one to avoid losing it |
|
// newWin = Instantiate<MaterialGraphEditWindow>(this); |
|
newWindow = EditorWindow.CreateWindow<MaterialGraphEditWindow>(typeof(MaterialGraphEditWindow), typeof(SceneView)); |
|
newWindow.Initialize(this); |
|
} |
|
else |
|
{ |
|
// the window is closing for good.. cleanup undo history for the graph object |
|
Undo.ClearUndo(graphObject); |
|
} |
|
|
|
graphObject = null; |
|
graphEditorView = null; |
|
|
|
// show new window if we have one |
|
if (newWindow != null) |
|
{ |
|
newWindow.Show(); |
|
newWindow.Focus(); |
|
} |
|
} |
|
|
|
public void PingAsset() |
|
{ |
|
if (selectedGuid != null) |
|
{ |
|
var path = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
var asset = AssetDatabase.LoadAssetAtPath<Object>(path); |
|
EditorGUIUtility.PingObject(asset); |
|
} |
|
} |
|
|
|
public bool IsGraphAssetCheckedOut() |
|
{ |
|
if (selectedGuid != null) |
|
{ |
|
var path = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
var asset = AssetDatabase.LoadAssetAtPath<Object>(path); |
|
if (!AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible)) |
|
return false; |
|
|
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
public void CheckoutAsset() |
|
{ |
|
if (selectedGuid != null) |
|
{ |
|
var path = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
var asset = AssetDatabase.LoadAssetAtPath<Object>(path); |
|
Task task = Provider.Checkout(asset, CheckoutMode.Both); |
|
task.Wait(); |
|
} |
|
} |
|
|
|
// returns true if the asset was succesfully saved |
|
public bool SaveAsset() |
|
{ |
|
bool saved = false; |
|
|
|
if (selectedGuid != null && graphObject != null) |
|
{ |
|
var path = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
if (string.IsNullOrEmpty(path) || graphObject == null) |
|
return false; |
|
|
|
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, graphObject.graph.GetNodes<SubGraphNode>(), "Save")) |
|
return false; |
|
|
|
ShaderGraphAnalytics.SendShaderGraphEvent(selectedGuid, graphObject.graph); |
|
|
|
var oldShader = AssetDatabase.LoadAssetAtPath<Shader>(path); |
|
if (oldShader != null) |
|
ShaderUtil.ClearShaderMessages(oldShader); |
|
|
|
var newFileContents = FileUtilities.WriteShaderGraphToDisk(path, graphObject.graph); |
|
if (newFileContents != null) |
|
{ |
|
saved = true; |
|
m_LastSerializedFileContents = newFileContents; |
|
AssetDatabase.ImportAsset(path); |
|
} |
|
|
|
OnSaveGraph(path); |
|
hasUnsavedChanges = false; |
|
} |
|
|
|
UpdateTitle(); |
|
|
|
return saved; |
|
} |
|
|
|
void OnSaveGraph(string path) |
|
{ |
|
if (GraphData.onSaveGraph == null) |
|
return; |
|
|
|
if (graphObject.graph.isSubGraph) |
|
return; |
|
|
|
var shader = AssetDatabase.LoadAssetAtPath<Shader>(path); |
|
if (shader == null) |
|
return; |
|
|
|
foreach (var target in graphObject.graph.activeTargets) |
|
{ |
|
GraphData.onSaveGraph(shader, target.saveContext); |
|
} |
|
} |
|
|
|
public void SaveAs() |
|
{ |
|
SaveAsImplementation(true); |
|
} |
|
|
|
// returns the asset path the file was saved to, or NULL if nothing was saved |
|
string SaveAsImplementation(bool openWhenSaved) |
|
{ |
|
string savedFilePath = null; |
|
|
|
if (selectedGuid != null && graphObject?.graph != null) |
|
{ |
|
var oldFilePath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
if (string.IsNullOrEmpty(oldFilePath) || graphObject == null) |
|
return null; |
|
|
|
// The asset's name needs to be removed from the path, otherwise SaveFilePanel assumes it's a folder |
|
string oldDirectory = Path.GetDirectoryName(oldFilePath); |
|
|
|
var extension = graphObject.graph.isSubGraph ? ShaderSubGraphImporter.Extension : ShaderGraphImporter.Extension; |
|
var newFilePath = EditorUtility.SaveFilePanelInProject("Save Graph As...", Path.GetFileNameWithoutExtension(oldFilePath), extension, "", oldDirectory); |
|
newFilePath = newFilePath.Replace(Application.dataPath, "Assets"); |
|
|
|
if (newFilePath != oldFilePath) |
|
{ |
|
if (!string.IsNullOrEmpty(newFilePath)) |
|
{ |
|
// If the newPath already exists, we are overwriting an existing file, and could be creating recursions. Let's check. |
|
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(newFilePath, graphObject.graph.GetNodes<SubGraphNode>(), "Save As")) |
|
return null; |
|
|
|
bool success = (FileUtilities.WriteShaderGraphToDisk(newFilePath, graphObject.graph) != null); |
|
AssetDatabase.ImportAsset(newFilePath); |
|
if (success) |
|
{ |
|
if (openWhenSaved) |
|
ShaderGraphImporterEditor.ShowGraphEditWindow(newFilePath); |
|
OnSaveGraph(newFilePath); |
|
savedFilePath = newFilePath; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
// saving to the current path |
|
if (SaveAsset()) |
|
{ |
|
graphObject.isDirty = false; |
|
savedFilePath = oldFilePath; |
|
} |
|
} |
|
} |
|
return savedFilePath; |
|
} |
|
|
|
public void ToSubGraph() |
|
{ |
|
var graphView = graphEditorView.graphView; |
|
|
|
string path; |
|
string sessionStateResult = SessionState.GetString(k_PrevSubGraphPathKey, k_PrevSubGraphPathDefaultValue); |
|
string pathToOriginSG = Path.GetDirectoryName(AssetDatabase.GUIDToAssetPath(selectedGuid)); |
|
|
|
if (!sessionStateResult.Equals(k_PrevSubGraphPathDefaultValue)) |
|
{ |
|
path = sessionStateResult; |
|
} |
|
else |
|
{ |
|
path = pathToOriginSG; |
|
} |
|
|
|
path = EditorUtility.SaveFilePanelInProject("Save Sub Graph", "New Shader Sub Graph", ShaderSubGraphImporter.Extension, "", path); |
|
path = path.Replace(Application.dataPath, "Assets"); |
|
|
|
// Friendly warning that the user is generating a subgraph that would overwrite the one they are currently working on. |
|
if (AssetDatabase.AssetPathToGUID(path) == selectedGuid) |
|
{ |
|
if (!EditorUtility.DisplayDialog("Overwrite Current Subgraph", "Do you want to overwrite this Sub Graph that you are currently working on? You cannot undo this operation.", "Yes", "Cancel")) |
|
{ |
|
path = ""; |
|
} |
|
} |
|
|
|
if (path.Length == 0) |
|
return; |
|
|
|
var nodes = graphView.selection.OfType<IShaderNodeView>().Where(x => !(x.node is PropertyNode || x.node is SubGraphOutputNode)).Select(x => x.node).Where(x => x.allowedInSubGraph).ToArray(); |
|
|
|
// Convert To Subgraph could create recursive reference loops if the target path already exists |
|
// Let's check for that here |
|
if (!string.IsNullOrEmpty(path)) |
|
{ |
|
if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, nodes.OfType<SubGraphNode>(), "Convert To SubGraph")) |
|
return; |
|
} |
|
|
|
graphObject.RegisterCompleteObjectUndo("Convert To Subgraph"); |
|
|
|
var bounds = Rect.MinMaxRect(float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity); |
|
foreach (var node in nodes) |
|
{ |
|
var center = node.drawState.position.center; |
|
bounds = Rect.MinMaxRect( |
|
Mathf.Min(bounds.xMin, center.x), |
|
Mathf.Min(bounds.yMin, center.y), |
|
Mathf.Max(bounds.xMax, center.x), |
|
Mathf.Max(bounds.yMax, center.y)); |
|
} |
|
var middle = bounds.center; |
|
bounds.center = Vector2.zero; |
|
|
|
// Collect graph inputs |
|
var graphInputs = graphView.selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput); |
|
var categories = graphView.selection.OfType<SGBlackboardCategory>().Select(x => x.userData as CategoryData); |
|
|
|
// Collect the property nodes and get the corresponding properties |
|
var propertyNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is PropertyNode)).Select(x => ((PropertyNode)x.node).property); |
|
var metaProperties = graphView.graph.properties.Where(x => propertyNodes.Contains(x)); |
|
|
|
// Collect the keyword nodes and get the corresponding keywords |
|
var keywordNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is KeywordNode)).Select(x => ((KeywordNode)x.node).keyword); |
|
var dropdownNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is DropdownNode)).Select(x => ((DropdownNode)x.node).dropdown); |
|
|
|
var metaKeywords = graphView.graph.keywords.Where(x => keywordNodes.Contains(x)); |
|
var metaDropdowns = graphView.graph.dropdowns.Where(x => dropdownNodes.Contains(x)); |
|
|
|
var copyPasteGraph = new CopyPasteGraph(graphView.selection.OfType<ShaderGroup>().Select(x => x.userData), |
|
nodes, |
|
graphView.selection.OfType<Edge>().Select(x => x.userData as Graphing.Edge), |
|
graphInputs, |
|
categories, |
|
metaProperties, |
|
metaKeywords, |
|
metaDropdowns, |
|
graphView.selection.OfType<StickyNote>().Select(x => x.userData), |
|
true, |
|
false); |
|
|
|
// why do we serialize and deserialize only to make copies of everything in the steps below? |
|
// is this just to clear out all non-serialized data? |
|
var deserialized = CopyPasteGraph.FromJson(MultiJson.Serialize(copyPasteGraph), graphView.graph); |
|
if (deserialized == null) |
|
return; |
|
|
|
var subGraph = new GraphData { isSubGraph = true, path = "Sub Graphs" }; |
|
var subGraphOutputNode = new SubGraphOutputNode(); |
|
{ |
|
var drawState = subGraphOutputNode.drawState; |
|
drawState.position = new Rect(new Vector2(bounds.xMax + 200f, 0f), drawState.position.size); |
|
subGraphOutputNode.drawState = drawState; |
|
} |
|
subGraph.AddNode(subGraphOutputNode); |
|
subGraph.outputNode = subGraphOutputNode; |
|
|
|
// Always copy deserialized keyword inputs |
|
foreach (ShaderKeyword keyword in deserialized.metaKeywords) |
|
{ |
|
var copiedInput = (ShaderKeyword)subGraph.AddCopyOfShaderInput(keyword); |
|
|
|
// Update the keyword nodes that depends on the copied keyword |
|
var dependentKeywordNodes = deserialized.GetNodes<KeywordNode>().Where(x => x.keyword == keyword); |
|
foreach (var node in dependentKeywordNodes) |
|
{ |
|
node.owner = graphView.graph; |
|
node.keyword = copiedInput; |
|
} |
|
} |
|
|
|
// Always copy deserialized dropdown inputs |
|
foreach (ShaderDropdown dropdown in deserialized.metaDropdowns) |
|
{ |
|
var copiedInput = (ShaderDropdown)subGraph.AddCopyOfShaderInput(dropdown); |
|
|
|
// Update the dropdown nodes that depends on the copied dropdown |
|
var dependentDropdownNodes = deserialized.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown); |
|
foreach (var node in dependentDropdownNodes) |
|
{ |
|
node.owner = graphView.graph; |
|
node.dropdown = copiedInput; |
|
} |
|
} |
|
|
|
foreach (GroupData groupData in deserialized.groups) |
|
{ |
|
subGraph.CreateGroup(groupData); |
|
} |
|
|
|
foreach (var node in deserialized.GetNodes<AbstractMaterialNode>()) |
|
{ |
|
var drawState = node.drawState; |
|
drawState.position = new Rect(drawState.position.position - middle, drawState.position.size); |
|
node.drawState = drawState; |
|
|
|
// Checking if the group guid is also being copied. |
|
// If not then nullify that guid |
|
if (node.group != null && !subGraph.groups.Contains(node.group)) |
|
{ |
|
node.group = null; |
|
} |
|
|
|
subGraph.AddNode(node); |
|
} |
|
|
|
foreach (var note in deserialized.stickyNotes) |
|
{ |
|
if (note.group != null && !subGraph.groups.Contains(note.group)) |
|
{ |
|
note.group = null; |
|
} |
|
|
|
subGraph.AddStickyNote(note); |
|
} |
|
|
|
// figure out what needs remapping |
|
var externalOutputSlots = new List<Graphing.Edge>(); |
|
var externalInputSlots = new List<Graphing.Edge>(); |
|
var passthroughSlots = new List<Graphing.Edge>(); |
|
foreach (var edge in deserialized.edges) |
|
{ |
|
var outputSlot = edge.outputSlot; |
|
var inputSlot = edge.inputSlot; |
|
|
|
var outputSlotExistsInSubgraph = subGraph.ContainsNode(outputSlot.node); |
|
var inputSlotExistsInSubgraph = subGraph.ContainsNode(inputSlot.node); |
|
|
|
// pasting nice internal links! |
|
if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph) |
|
{ |
|
subGraph.Connect(outputSlot, inputSlot); |
|
} |
|
// one edge needs to go to outside world |
|
else if (outputSlotExistsInSubgraph) |
|
{ |
|
externalInputSlots.Add(edge); |
|
} |
|
else if (inputSlotExistsInSubgraph) |
|
{ |
|
externalOutputSlots.Add(edge); |
|
} |
|
else |
|
{ |
|
externalInputSlots.Add(edge); |
|
externalOutputSlots.Add(edge); |
|
passthroughSlots.Add(edge); |
|
} |
|
} |
|
|
|
// Find the unique edges coming INTO the graph |
|
var uniqueIncomingEdges = externalOutputSlots.GroupBy( |
|
edge => edge.outputSlot, |
|
edge => edge, |
|
(key, edges) => new { slotRef = key, edges = edges.ToList() }); |
|
|
|
var externalInputNeedingConnection = new List<KeyValuePair<IEdge, AbstractShaderProperty>>(); |
|
|
|
var amountOfProps = uniqueIncomingEdges.Count(); |
|
const int height = 40; |
|
const int subtractHeight = 20; |
|
var propPos = new Vector2(0, -((amountOfProps / 2) + height) - subtractHeight); |
|
|
|
var passthroughSlotRefLookup = new Dictionary<SlotReference, SlotReference>(); |
|
|
|
var passedInProperties = new Dictionary<AbstractShaderProperty, AbstractShaderProperty>(); |
|
foreach (var group in uniqueIncomingEdges) |
|
{ |
|
var sr = group.slotRef; |
|
var fromNode = sr.node; |
|
var fromSlot = sr.slot; |
|
|
|
var materialGraph = graphObject.graph; |
|
var fromProperty = fromNode is PropertyNode fromPropertyNode |
|
? materialGraph.properties.FirstOrDefault(p => p == fromPropertyNode.property) |
|
: null; |
|
|
|
AbstractShaderProperty prop; |
|
if (fromProperty != null && passedInProperties.TryGetValue(fromProperty, out prop)) |
|
{ |
|
} |
|
else |
|
{ |
|
switch (fromSlot.concreteValueType) |
|
{ |
|
case ConcreteSlotValueType.Texture2D: |
|
prop = new Texture2DShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Texture2DArray: |
|
prop = new Texture2DArrayShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Texture3D: |
|
prop = new Texture3DShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Cubemap: |
|
prop = new CubemapShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Vector4: |
|
prop = new Vector4ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Vector3: |
|
prop = new Vector3ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Vector2: |
|
prop = new Vector2ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Vector1: |
|
prop = new Vector1ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Boolean: |
|
prop = new BooleanShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Matrix2: |
|
prop = new Matrix2ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Matrix3: |
|
prop = new Matrix3ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Matrix4: |
|
prop = new Matrix4ShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.SamplerState: |
|
prop = new SamplerStateShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.Gradient: |
|
prop = new GradientShaderProperty(); |
|
break; |
|
case ConcreteSlotValueType.VirtualTexture: |
|
prop = new VirtualTextureShaderProperty() |
|
{ |
|
// also copy the VT settings over from the original property (if there is one) |
|
value = (fromProperty as VirtualTextureShaderProperty)?.value ?? new SerializableVirtualTexture() |
|
}; |
|
break; |
|
default: |
|
throw new ArgumentOutOfRangeException(); |
|
} |
|
|
|
var propName = fromProperty != null |
|
? fromProperty.displayName |
|
: fromSlot.concreteValueType.ToString(); |
|
prop.SetDisplayNameAndSanitizeForGraph(subGraph, propName); |
|
|
|
subGraph.AddGraphInput(prop); |
|
if (fromProperty != null) |
|
{ |
|
passedInProperties.Add(fromProperty, prop); |
|
} |
|
} |
|
|
|
var propNode = new PropertyNode(); |
|
{ |
|
var drawState = propNode.drawState; |
|
drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f) + propPos, |
|
drawState.position.size); |
|
propPos += new Vector2(0, height); |
|
propNode.drawState = drawState; |
|
} |
|
subGraph.AddNode(propNode); |
|
propNode.property = prop; |
|
|
|
|
|
Vector2 avg = Vector2.zero; |
|
foreach (var edge in group.edges) |
|
{ |
|
if (passthroughSlots.Contains(edge) && !passthroughSlotRefLookup.ContainsKey(sr)) |
|
{ |
|
passthroughSlotRefLookup.Add(sr, new SlotReference(propNode, PropertyNode.OutputSlotId)); |
|
} |
|
else |
|
{ |
|
subGraph.Connect( |
|
new SlotReference(propNode, PropertyNode.OutputSlotId), |
|
edge.inputSlot); |
|
|
|
int i; |
|
var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList(); |
|
|
|
for (i = 0; i < inputs.Count; ++i) |
|
{ |
|
if (inputs[i].slotReference.slotId == edge.inputSlot.slotId) |
|
{ |
|
break; |
|
} |
|
} |
|
avg += new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y + 30f * i); |
|
} |
|
//we collapse input properties so dont add edges that are already being added |
|
if (!externalInputNeedingConnection.Any(x => x.Key.outputSlot.slot == edge.outputSlot.slot && x.Value == prop)) |
|
{ |
|
externalInputNeedingConnection.Add(new KeyValuePair<IEdge, AbstractShaderProperty>(edge, prop)); |
|
} |
|
} |
|
avg /= group.edges.Count; |
|
var pos = avg - new Vector2(150f, 0f); |
|
propNode.drawState = new DrawState() |
|
{ |
|
position = new Rect(pos, propNode.drawState.position.size), |
|
expanded = propNode.drawState.expanded |
|
}; |
|
} |
|
|
|
var uniqueOutgoingEdges = externalInputSlots.GroupBy( |
|
edge => edge.outputSlot, |
|
edge => edge, |
|
(key, edges) => new { slot = key, edges = edges.ToList() }); |
|
|
|
var externalOutputsNeedingConnection = new List<KeyValuePair<IEdge, IEdge>>(); |
|
foreach (var group in uniqueOutgoingEdges) |
|
{ |
|
var outputNode = subGraph.outputNode as SubGraphOutputNode; |
|
|
|
AbstractMaterialNode node = group.edges[0].outputSlot.node; |
|
MaterialSlot slot = node.FindSlot<MaterialSlot>(group.edges[0].outputSlot.slotId); |
|
var slotId = outputNode.AddSlot(slot.concreteValueType); |
|
|
|
var inputSlotRef = new SlotReference(outputNode, slotId); |
|
|
|
foreach (var edge in group.edges) |
|
{ |
|
var newEdge = subGraph.Connect(passthroughSlotRefLookup.TryGetValue(edge.outputSlot, out SlotReference remap) ? remap : edge.outputSlot, inputSlotRef); |
|
externalOutputsNeedingConnection.Add(new KeyValuePair<IEdge, IEdge>(edge, newEdge)); |
|
} |
|
} |
|
|
|
if (FileUtilities.WriteShaderGraphToDisk(path, subGraph) != null) |
|
AssetDatabase.ImportAsset(path); |
|
|
|
// Store path for next time |
|
if (!pathToOriginSG.Equals(Path.GetDirectoryName(path))) |
|
{ |
|
SessionState.SetString(k_PrevSubGraphPathKey, Path.GetDirectoryName(path)); |
|
} |
|
else |
|
{ |
|
// Or continue to make it so that next time it will open up in the converted-from SG's directory |
|
SessionState.EraseString(k_PrevSubGraphPathKey); |
|
} |
|
|
|
var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(SubGraphAsset)) as SubGraphAsset; |
|
if (loadedSubGraph == null) |
|
return; |
|
|
|
var subGraphNode = new SubGraphNode(); |
|
var ds = subGraphNode.drawState; |
|
ds.position = new Rect(middle - new Vector2(100f, 150f), Vector2.zero); |
|
subGraphNode.drawState = ds; |
|
|
|
// Add the subgraph into the group if the nodes was all in the same group group |
|
var firstNode = copyPasteGraph.GetNodes<AbstractMaterialNode>().FirstOrDefault(); |
|
if (firstNode != null && copyPasteGraph.GetNodes<AbstractMaterialNode>().All(x => x.group == firstNode.group)) |
|
{ |
|
subGraphNode.group = firstNode.group; |
|
} |
|
|
|
subGraphNode.asset = loadedSubGraph; |
|
graphObject.graph.AddNode(subGraphNode); |
|
|
|
foreach (var edgeMap in externalInputNeedingConnection) |
|
{ |
|
graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode, edgeMap.Value.guid.GetHashCode())); |
|
} |
|
|
|
foreach (var edgeMap in externalOutputsNeedingConnection) |
|
{ |
|
graphObject.graph.Connect(new SlotReference(subGraphNode, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot); |
|
} |
|
|
|
graphObject.graph.RemoveElements( |
|
graphView.selection.OfType<IShaderNodeView>().Select(x => x.node).Where(x => !(x is PropertyNode || x is SubGraphOutputNode) && x.allowedInSubGraph).ToArray(), |
|
new IEdge[] { }, |
|
new GroupData[] { }, |
|
graphView.selection.OfType<StickyNote>().Select(x => x.userData).ToArray()); |
|
|
|
List<GraphElement> moved = new List<GraphElement>(); |
|
foreach (var nodeView in graphView.selection.OfType<IShaderNodeView>()) |
|
{ |
|
var node = nodeView.node; |
|
if (graphView.graph.removedNodes.Contains(node) || node is SubGraphOutputNode) |
|
{ |
|
continue; |
|
} |
|
|
|
var edges = graphView.graph.GetEdges(node); |
|
int numEdges = edges.Count(); |
|
if (numEdges == 0) |
|
{ |
|
graphView.graph.RemoveNode(node); |
|
} |
|
else if (numEdges == 1 && edges.First().inputSlot.node != node) //its an output edge |
|
{ |
|
var edge = edges.First(); |
|
int i; |
|
var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList(); |
|
for (i = 0; i < inputs.Count; ++i) |
|
{ |
|
if (inputs[i].slotReference.slotId == edge.inputSlot.slotId) |
|
{ |
|
break; |
|
} |
|
} |
|
node.drawState = new DrawState() |
|
{ |
|
position = new Rect(new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y) - new Vector2(150f, -30f * i), node.drawState.position.size), |
|
expanded = node.drawState.expanded |
|
}; |
|
(nodeView as GraphElement).SetPosition(node.drawState.position); |
|
} |
|
} |
|
graphObject.graph.ValidateGraph(); |
|
} |
|
|
|
public void Initialize(MaterialGraphEditWindow other) |
|
{ |
|
// create a new window that copies the entire editor state of an existing window |
|
// this function is used to "reopen" an editor window that is closing, but where the user has canceled the close |
|
// for example, if the graph of a closing window was dirty, but could not be saved |
|
try |
|
{ |
|
selectedGuid = other.selectedGuid; |
|
|
|
graphObject = CreateInstance<GraphObject>(); |
|
graphObject.hideFlags = HideFlags.HideAndDontSave; |
|
graphObject.graph = other.graphObject.graph; |
|
|
|
graphObject.graph.messageManager = this.messageManager; |
|
|
|
UpdateTitle(); |
|
|
|
Repaint(); |
|
} |
|
catch (Exception e) |
|
{ |
|
Debug.LogException(e); |
|
m_HasError = true; |
|
m_GraphEditorView = null; |
|
graphObject = null; |
|
throw; |
|
} |
|
} |
|
|
|
private static readonly ProfilerMarker GraphLoadMarker = new ProfilerMarker("GraphLoad"); |
|
private static readonly ProfilerMarker CreateGraphEditorViewMarker = new ProfilerMarker("CreateGraphEditorView"); |
|
public void Initialize(string assetGuid) |
|
{ |
|
try |
|
{ |
|
m_ColorSpace = PlayerSettings.colorSpace; |
|
m_RenderPipelineAsset = GraphicsSettings.renderPipelineAsset; |
|
|
|
var asset = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(assetGuid)); |
|
if (asset == null) |
|
return; |
|
|
|
if (!EditorUtility.IsPersistent(asset)) |
|
return; |
|
|
|
if (selectedGuid == assetGuid) |
|
return; |
|
|
|
|
|
var path = AssetDatabase.GetAssetPath(asset); |
|
var extension = Path.GetExtension(path); |
|
if (extension == null) |
|
return; |
|
// Path.GetExtension returns the extension prefixed with ".", so we remove it. We force lower case such that |
|
// the comparison will be case-insensitive. |
|
extension = extension.Substring(1).ToLowerInvariant(); |
|
bool isSubGraph; |
|
switch (extension) |
|
{ |
|
case ShaderGraphImporter.Extension: |
|
isSubGraph = false; |
|
break; |
|
case ShaderSubGraphImporter.Extension: |
|
isSubGraph = true; |
|
break; |
|
default: |
|
return; |
|
} |
|
|
|
selectedGuid = assetGuid; |
|
string graphName = Path.GetFileNameWithoutExtension(path); |
|
|
|
using (GraphLoadMarker.Auto()) |
|
{ |
|
m_LastSerializedFileContents = File.ReadAllText(path, Encoding.UTF8); |
|
graphObject = CreateInstance<GraphObject>(); |
|
graphObject.hideFlags = HideFlags.HideAndDontSave; |
|
graphObject.graph = new GraphData |
|
{ |
|
assetGuid = assetGuid, |
|
isSubGraph = isSubGraph, |
|
messageManager = messageManager |
|
}; |
|
MultiJson.Deserialize(graphObject.graph, m_LastSerializedFileContents); |
|
graphObject.graph.OnEnable(); |
|
graphObject.graph.ValidateGraph(); |
|
} |
|
|
|
using (CreateGraphEditorViewMarker.Auto()) |
|
{ |
|
graphEditorView = new GraphEditorView(this, m_GraphObject.graph, messageManager, graphName) |
|
{ |
|
viewDataKey = selectedGuid, |
|
}; |
|
} |
|
|
|
UpdateTitle(); |
|
|
|
Repaint(); |
|
} |
|
catch (Exception) |
|
{ |
|
m_HasError = true; |
|
m_GraphEditorView = null; |
|
graphObject = null; |
|
throw; |
|
} |
|
} |
|
|
|
// returns contents of the asset file, or null if any exception occurred |
|
private string ReadAssetFile() |
|
{ |
|
var filePath = AssetDatabase.GUIDToAssetPath(selectedGuid); |
|
return FileUtilities.SafeReadAllText(filePath); |
|
} |
|
|
|
// returns true when the user is OK with closing the window or application (either they've saved dirty content, or are ok with losing it) |
|
// returns false when the user wants to cancel closing the window or application |
|
private bool PromptSaveIfDirtyOnQuit() |
|
{ |
|
// only bother unless we've actually got data to preserve |
|
if (graphObject?.graph != null) |
|
{ |
|
// if the asset has been deleted, ask the user what to do |
|
if (!AssetFileExists()) |
|
return DisplayDeletedFromDiskDialog(false); |
|
|
|
// If there are unsaved modifications, ask the user what to do. |
|
// If the editor has already handled this check we'll no longer have unsaved changes |
|
// (either they saved or they discarded, both of which will set hasUnsavedChanges to false). |
|
if (hasUnsavedChanges) |
|
{ |
|
int option = EditorUtility.DisplayDialogComplex( |
|
"Shader Graph Has Been Modified", |
|
GetSaveChangesMessage(), |
|
"Save", "Cancel", "Discard Changes"); |
|
|
|
if (option == 0) // save |
|
{ |
|
return SaveAsset(); |
|
} |
|
else if (option == 1) // cancel (or escape/close dialog) |
|
{ |
|
return false; |
|
} |
|
else if (option == 2) // discard |
|
{ |
|
return true; |
|
} |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
private string GetSaveChangesMessage() |
|
{ |
|
return "Do you want to save the changes you made in the Shader Graph?\n\n" + |
|
AssetDatabase.GUIDToAssetPath(selectedGuid) + |
|
"\n\nYour changes will be lost if you don't save them."; |
|
} |
|
|
|
public override void SaveChanges() |
|
{ |
|
base.SaveChanges(); |
|
SaveAsset(); |
|
} |
|
|
|
void OnGeometryChanged(GeometryChangedEvent evt) |
|
{ |
|
if (graphEditorView == null) |
|
return; |
|
|
|
// this callback is only so we can run post-layout behaviors after the graph loads for the first time |
|
// we immediately unregister it so it doesn't get called again |
|
graphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged); |
|
if (m_FrameAllAfterLayout) |
|
graphEditorView.graphView.FrameAll(); |
|
m_FrameAllAfterLayout = false; |
|
} |
|
} |
|
}
|
|
|