Ingen beskrivning
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.

MaterialGraphEditWindow.cs 54KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using UnityEngine;
  7. using UnityEditor.Graphing;
  8. using UnityEditor.Graphing.Util;
  9. using Debug = UnityEngine.Debug;
  10. using Object = UnityEngine.Object;
  11. using UnityEngine.Rendering;
  12. using UnityEditor.UIElements;
  13. using Edge = UnityEditor.Experimental.GraphView.Edge;
  14. using UnityEditor.Experimental.GraphView;
  15. using UnityEditor.ShaderGraph.Internal;
  16. using UnityEditor.ShaderGraph.Serialization;
  17. using UnityEngine.UIElements;
  18. using UnityEditor.VersionControl;
  19. using Unity.Profiling;
  20. using UnityEngine.Assertions;
  21. namespace UnityEditor.ShaderGraph.Drawing
  22. {
  23. class MaterialGraphEditWindow : EditorWindow
  24. {
  25. // For conversion to Sub Graph: keys for remembering the user's desired path
  26. const string k_PrevSubGraphPathKey = "SHADER_GRAPH_CONVERT_TO_SUB_GRAPH_PATH";
  27. const string k_PrevSubGraphPathDefaultValue = "?"; // Special character that NTFS does not allow, so that no directory could match it.
  28. [SerializeField]
  29. string m_Selected;
  30. [SerializeField]
  31. GraphObject m_GraphObject;
  32. // this stores the contents of the file on disk, as of the last time we saved or loaded it from disk
  33. [SerializeField]
  34. string m_LastSerializedFileContents;
  35. [NonSerialized]
  36. HashSet<string> m_ChangedFileDependencyGUIDs = new HashSet<string>();
  37. ColorSpace m_ColorSpace;
  38. RenderPipelineAsset m_RenderPipelineAsset;
  39. [NonSerialized]
  40. bool m_FrameAllAfterLayout;
  41. [NonSerialized]
  42. bool m_HasError;
  43. [NonSerialized]
  44. bool m_ProTheme;
  45. [NonSerialized]
  46. int m_customInterpWarn;
  47. [NonSerialized]
  48. int m_customInterpErr;
  49. [SerializeField]
  50. bool m_AssetMaybeChangedOnDisk;
  51. [SerializeField]
  52. bool m_AssetMaybeDeleted;
  53. internal bool WereWindowResourcesDisposed { get; private set; }
  54. MessageManager m_MessageManager;
  55. MessageManager messageManager
  56. {
  57. get { return m_MessageManager ?? (m_MessageManager = new MessageManager()); }
  58. }
  59. GraphEditorView m_GraphEditorView;
  60. internal GraphEditorView graphEditorView
  61. {
  62. get { return m_GraphEditorView; }
  63. private set
  64. {
  65. if (m_GraphEditorView != null)
  66. {
  67. m_GraphEditorView.RemoveFromHierarchy();
  68. m_GraphEditorView.Dispose();
  69. }
  70. m_GraphEditorView = value;
  71. if (m_GraphEditorView != null)
  72. {
  73. m_GraphEditorView.saveRequested += () => SaveAsset();
  74. m_GraphEditorView.saveAsRequested += SaveAs;
  75. m_GraphEditorView.convertToSubgraphRequested += ToSubGraph;
  76. m_GraphEditorView.showInProjectRequested += PingAsset;
  77. m_GraphEditorView.isCheckedOut += IsGraphAssetCheckedOut;
  78. m_GraphEditorView.checkOut += CheckoutAsset;
  79. m_GraphEditorView.RegisterCallback<GeometryChangedEvent>(OnGeometryChanged);
  80. m_FrameAllAfterLayout = true;
  81. this.rootVisualElement.Add(graphEditorView);
  82. }
  83. }
  84. }
  85. internal GraphObject graphObject
  86. {
  87. get { return m_GraphObject; }
  88. set
  89. {
  90. if (m_GraphObject != null)
  91. DestroyImmediate(m_GraphObject);
  92. m_GraphObject = value;
  93. }
  94. }
  95. public string selectedGuid
  96. {
  97. get { return m_Selected; }
  98. private set
  99. {
  100. m_Selected = value;
  101. }
  102. }
  103. public string assetName
  104. {
  105. get { return titleContent.text; }
  106. }
  107. bool AssetFileExists()
  108. {
  109. var assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  110. return File.Exists(assetPath);
  111. }
  112. // returns true when the graph has been successfully saved, or the user has indicated they are ok with discarding the local graph
  113. // returns false when saving has failed
  114. bool DisplayDeletedFromDiskDialog(bool reopen = true)
  115. {
  116. // first double check if we've actually been deleted
  117. bool saved = false;
  118. bool okToClose = false;
  119. string originalAssetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  120. while (true)
  121. {
  122. int option = EditorUtility.DisplayDialogComplex(
  123. "Graph removed from project",
  124. "The file has been deleted or removed from the project folder.\n\n" +
  125. originalAssetPath +
  126. "\n\nWould you like to save your Graph Asset?",
  127. "Save As...", "Cancel", "Discard Graph and Close Window");
  128. if (option == 0)
  129. {
  130. string savedPath = SaveAsImplementation(false);
  131. if (savedPath != null)
  132. {
  133. saved = true;
  134. // either close or reopen the local window editor
  135. graphObject = null;
  136. selectedGuid = (reopen ? AssetDatabase.AssetPathToGUID(savedPath) : null);
  137. break;
  138. }
  139. }
  140. else if (option == 2)
  141. {
  142. okToClose = true;
  143. graphObject = null;
  144. selectedGuid = null;
  145. break;
  146. }
  147. else if (option == 1)
  148. {
  149. // continue in deleted state...
  150. break;
  151. }
  152. }
  153. return (saved || okToClose);
  154. }
  155. void Update()
  156. {
  157. if (m_HasError)
  158. return;
  159. bool updateTitle = false;
  160. if (m_AssetMaybeDeleted)
  161. {
  162. m_AssetMaybeDeleted = false;
  163. if (AssetFileExists())
  164. {
  165. // it exists... just to be sure, let's check if it changed
  166. m_AssetMaybeChangedOnDisk = true;
  167. }
  168. else
  169. {
  170. // it was really deleted, ask the user what to do
  171. bool handled = DisplayDeletedFromDiskDialog(true);
  172. }
  173. updateTitle = true;
  174. }
  175. if (PlayerSettings.colorSpace != m_ColorSpace)
  176. {
  177. graphEditorView = null;
  178. m_ColorSpace = PlayerSettings.colorSpace;
  179. }
  180. if (GraphicsSettings.currentRenderPipeline != m_RenderPipelineAsset)
  181. {
  182. graphEditorView = null;
  183. m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
  184. }
  185. if (EditorGUIUtility.isProSkin != m_ProTheme)
  186. {
  187. if (graphObject != null && graphObject.graph != null)
  188. {
  189. updateTitle = true; // trigger icon swap
  190. m_ProTheme = EditorGUIUtility.isProSkin;
  191. }
  192. }
  193. bool revalidate = false;
  194. if (m_customInterpWarn != ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold)
  195. {
  196. m_customInterpWarn = ShaderGraphProjectSettings.instance.customInterpolatorWarningThreshold;
  197. revalidate = true;
  198. }
  199. if (m_customInterpErr != ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold)
  200. {
  201. m_customInterpErr = ShaderGraphProjectSettings.instance.customInterpolatorErrorThreshold;
  202. revalidate = true;
  203. }
  204. if (revalidate)
  205. {
  206. graphEditorView?.graphView?.graph?.ValidateGraph();
  207. }
  208. if (m_AssetMaybeChangedOnDisk)
  209. {
  210. m_AssetMaybeChangedOnDisk = false;
  211. // if we don't have any graph, then it doesn't really matter if the file on disk changed or not
  212. // as we're going to reload it below anyways
  213. if (graphObject?.graph != null)
  214. {
  215. // check if it actually did change on disk
  216. if (FileOnDiskHasChanged())
  217. {
  218. // don't worry people about "losing changes" unless there are changes to lose
  219. bool graphChanged = GraphHasChangedSinceLastSerialization();
  220. if (EditorUtility.DisplayDialog(
  221. "Graph has changed on disk",
  222. AssetDatabase.GUIDToAssetPath(selectedGuid) + "\n\n" +
  223. (graphChanged ? "Do you want to reload it and lose the changes made in the graph?" : "Do you want to reload it?"),
  224. graphChanged ? "Discard Changes And Reload" : "Reload",
  225. "Don't Reload"))
  226. {
  227. // clear graph, trigger reload
  228. graphObject = null;
  229. }
  230. }
  231. }
  232. updateTitle = true;
  233. }
  234. try
  235. {
  236. if (graphObject == null && selectedGuid != null)
  237. {
  238. var guid = selectedGuid;
  239. selectedGuid = null;
  240. Initialize(guid);
  241. }
  242. if (graphObject == null)
  243. {
  244. Close();
  245. return;
  246. }
  247. var materialGraph = graphObject.graph as GraphData;
  248. if (materialGraph == null)
  249. return;
  250. if (graphEditorView == null)
  251. {
  252. messageManager.ClearAll();
  253. materialGraph.messageManager = messageManager;
  254. string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  255. string graphName = Path.GetFileNameWithoutExtension(assetPath);
  256. graphEditorView = new GraphEditorView(this, materialGraph, messageManager, graphName)
  257. {
  258. viewDataKey = selectedGuid,
  259. };
  260. m_ColorSpace = PlayerSettings.colorSpace;
  261. m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
  262. graphObject.Validate();
  263. // update blackboard title for the new graphEditorView
  264. updateTitle = true;
  265. }
  266. if (m_ChangedFileDependencyGUIDs.Count > 0 && graphObject != null && graphObject.graph != null)
  267. {
  268. bool reloadedSomething = false;
  269. foreach (var guid in m_ChangedFileDependencyGUIDs)
  270. {
  271. if (AssetDatabase.GUIDToAssetPath(guid) != null)
  272. {
  273. // update preview for changed textures
  274. graphEditorView?.previewManager?.ReloadChangedFiles(guid);
  275. }
  276. }
  277. var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
  278. foreach (var subGraphNode in subGraphNodes)
  279. {
  280. var reloaded = subGraphNode.Reload(m_ChangedFileDependencyGUIDs);
  281. reloadedSomething |= reloaded;
  282. }
  283. if (subGraphNodes.Count() > 0)
  284. {
  285. // Keywords always need to be updated to test against variant limit
  286. // No Keywords may indicate removal and this may have now made the Graph valid again
  287. // Need to validate Graph to clear errors in this case
  288. materialGraph.OnKeywordChanged();
  289. UpdateDropdownEntries();
  290. materialGraph.OnDropdownChanged();
  291. }
  292. foreach (var customFunctionNode in graphObject.graph.GetNodes<CustomFunctionNode>())
  293. {
  294. var reloaded = customFunctionNode.Reload(m_ChangedFileDependencyGUIDs);
  295. reloadedSomething |= reloaded;
  296. }
  297. // reloading files may change serialization
  298. if (reloadedSomething)
  299. {
  300. updateTitle = true;
  301. // may also need to re-run validation/concretization
  302. graphObject.Validate();
  303. }
  304. m_ChangedFileDependencyGUIDs.Clear();
  305. }
  306. var wasUndoRedoPerformed = graphObject.wasUndoRedoPerformed;
  307. if (wasUndoRedoPerformed)
  308. {
  309. graphEditorView.HandleGraphChanges(true);
  310. graphObject.graph.ClearChanges();
  311. graphObject.HandleUndoRedo();
  312. }
  313. if (graphObject.isDirty || wasUndoRedoPerformed)
  314. {
  315. updateTitle = true;
  316. graphObject.isDirty = false;
  317. hasUnsavedChanges = false;
  318. }
  319. // Called again to handle changes from deserialization in case an undo/redo was performed
  320. graphEditorView.HandleGraphChanges(wasUndoRedoPerformed);
  321. graphObject.graph.ClearChanges();
  322. if (updateTitle)
  323. UpdateTitle();
  324. }
  325. catch (Exception e)
  326. {
  327. m_HasError = true;
  328. m_GraphEditorView = null;
  329. graphObject = null;
  330. Debug.LogException(e);
  331. throw;
  332. }
  333. }
  334. public void ReloadSubGraphsOnNextUpdate(List<string> changedFileGUIDs)
  335. {
  336. foreach (var changedFileGUID in changedFileGUIDs)
  337. {
  338. m_ChangedFileDependencyGUIDs.Add(changedFileGUID);
  339. }
  340. }
  341. void UpdateDropdownEntries()
  342. {
  343. var subGraphNodes = graphObject.graph.GetNodes<SubGraphNode>();
  344. foreach (var subGraphNode in subGraphNodes)
  345. {
  346. var nodeView = graphEditorView.graphView.nodes.ToList().OfType<IShaderNodeView>()
  347. .FirstOrDefault(p => p.node != null && p.node == subGraphNode);
  348. if (nodeView != null)
  349. {
  350. nodeView.UpdateDropdownEntries();
  351. }
  352. }
  353. }
  354. void OnEnable()
  355. {
  356. this.SetAntiAliasing(4);
  357. }
  358. void OnDisable()
  359. {
  360. m_GraphEditorView?.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
  361. m_GraphEditorView?.Dispose();
  362. messageManager.ClearAll();
  363. m_GraphEditorView = null;
  364. m_GraphObject = null;
  365. m_MessageManager = null;
  366. m_RenderPipelineAsset = null;
  367. Resources.UnloadUnusedAssets();
  368. WereWindowResourcesDisposed = true;
  369. }
  370. // 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)
  371. internal bool FileOnDiskHasChanged()
  372. {
  373. var currentFileJson = ReadAssetFile();
  374. return !string.Equals(currentFileJson, m_LastSerializedFileContents, StringComparison.Ordinal);
  375. }
  376. // returns true only when the graph in this window would serialize different from the last time we loaded or saved it
  377. internal bool GraphHasChangedSinceLastSerialization()
  378. {
  379. Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
  380. var currentGraphJson = MultiJson.Serialize(graphObject.graph);
  381. return !string.Equals(currentGraphJson, m_LastSerializedFileContents, StringComparison.Ordinal);
  382. }
  383. // returns true only when saving the graph in this window would serialize different from the file on disk
  384. internal bool GraphIsDifferentFromFileOnDisk()
  385. {
  386. Assert.IsTrue(graphObject?.graph != null); // this should be checked by calling code
  387. var currentGraphJson = MultiJson.Serialize(graphObject.graph);
  388. var currentFileJson = ReadAssetFile();
  389. return !string.Equals(currentGraphJson, currentFileJson, StringComparison.Ordinal);
  390. }
  391. // NOTE: we're using the AssetPostprocessor Asset Import and Deleted callbacks as a proxy for detecting file changes
  392. // We could probably replace both m_AssetMaybeDeleted and m_AssetMaybeChangedOnDisk with a combined "need to check the real status of the file" flag
  393. public void CheckForChanges()
  394. {
  395. if (!m_AssetMaybeDeleted && graphObject?.graph != null)
  396. {
  397. m_AssetMaybeChangedOnDisk = true;
  398. UpdateTitle();
  399. }
  400. }
  401. public void AssetWasDeleted()
  402. {
  403. m_AssetMaybeDeleted = true;
  404. UpdateTitle();
  405. }
  406. public void UpdateTitle()
  407. {
  408. string assetPath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  409. string shaderName = Path.GetFileNameWithoutExtension(assetPath);
  410. // update blackboard title (before we add suffixes)
  411. if (graphEditorView != null)
  412. graphEditorView.assetName = shaderName;
  413. // build the window title (with suffixes)
  414. string title = shaderName;
  415. if (graphObject?.graph == null)
  416. title = title + " (nothing loaded)";
  417. else
  418. {
  419. if (GraphHasChangedSinceLastSerialization())
  420. {
  421. hasUnsavedChanges = true;
  422. // This is the message EditorWindow will show when prompting to close while dirty
  423. saveChangesMessage = GetSaveChangesMessage();
  424. }
  425. else
  426. {
  427. hasUnsavedChanges = false;
  428. saveChangesMessage = "";
  429. }
  430. if (!AssetFileExists())
  431. title = title + " (deleted)";
  432. }
  433. // get window icon
  434. bool isSubGraph = graphObject?.graph?.isSubGraph ?? false;
  435. Texture2D icon;
  436. {
  437. string theme = EditorGUIUtility.isProSkin ? "_dark" : "_light";
  438. if (isSubGraph)
  439. icon = Resources.Load<Texture2D>("Icons/sg_subgraph_icon_gray" + theme);
  440. else
  441. icon = Resources.Load<Texture2D>("Icons/sg_graph_icon_gray" + theme);
  442. }
  443. titleContent = new GUIContent(title, icon);
  444. }
  445. void OnDestroy()
  446. {
  447. // Prompting the user if they want to close is mostly handled via the EditorWindow's system (hasUnsavedChanges).
  448. // There's unfortunately a code path (Reload Window) that doesn't go through this path. The old logic is left
  449. // here as a fallback to catch this. This has one edge case with "Reload Window" -> "Cancel" which will produce
  450. // two shader graph windows: one unmodified (that the editor opens) and one modified (that we open below).
  451. // we are closing the shadergraph window
  452. MaterialGraphEditWindow newWindow = null;
  453. if (!PromptSaveIfDirtyOnQuit())
  454. {
  455. // user does not want to close the window.
  456. // we can't stop the close from this code path though..
  457. // all we can do is open a new window and transfer our data to the new one to avoid losing it
  458. // newWin = Instantiate<MaterialGraphEditWindow>(this);
  459. newWindow = EditorWindow.CreateWindow<MaterialGraphEditWindow>(typeof(MaterialGraphEditWindow), typeof(SceneView));
  460. newWindow.Initialize(this);
  461. }
  462. else
  463. {
  464. // the window is closing for good.. cleanup undo history for the graph object
  465. Undo.ClearUndo(graphObject);
  466. }
  467. // Discard any unsaved modification on the generated material
  468. if (graphObject && graphObject.materialArtifact && EditorUtility.IsDirty(graphObject.materialArtifact))
  469. {
  470. var material = new Material(AssetDatabase.LoadAssetAtPath<Shader>(AssetDatabase.GUIDToAssetPath(graphObject.AssetGuid)));
  471. graphObject.materialArtifact.CopyPropertiesFromMaterial(material);
  472. CoreUtils.Destroy(material);
  473. }
  474. graphObject = null;
  475. graphEditorView = null;
  476. // show new window if we have one
  477. if (newWindow != null)
  478. {
  479. newWindow.Show();
  480. newWindow.Focus();
  481. }
  482. }
  483. public void PingAsset()
  484. {
  485. if (selectedGuid != null)
  486. {
  487. var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
  488. var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
  489. EditorGUIUtility.PingObject(asset);
  490. }
  491. }
  492. public bool IsGraphAssetCheckedOut()
  493. {
  494. if (selectedGuid != null)
  495. {
  496. var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
  497. var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
  498. if (!AssetDatabase.IsOpenForEdit(asset, StatusQueryOptions.UseCachedIfPossible))
  499. return false;
  500. return true;
  501. }
  502. return false;
  503. }
  504. public void CheckoutAsset()
  505. {
  506. if (selectedGuid != null)
  507. {
  508. var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
  509. var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
  510. Task task = Provider.Checkout(asset, CheckoutMode.Both);
  511. task.Wait();
  512. }
  513. }
  514. // returns true if the asset was succesfully saved
  515. public bool SaveAsset()
  516. {
  517. bool saved = false;
  518. if (selectedGuid != null && graphObject != null)
  519. {
  520. var path = AssetDatabase.GUIDToAssetPath(selectedGuid);
  521. if (string.IsNullOrEmpty(path) || graphObject == null)
  522. return false;
  523. if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, graphObject.graph.GetNodes<SubGraphNode>(), "Save"))
  524. return false;
  525. ShaderGraphAnalytics.SendShaderGraphEvent(selectedGuid, graphObject.graph);
  526. var oldShader = AssetDatabase.LoadAssetAtPath<Shader>(path);
  527. if (oldShader != null)
  528. ShaderUtil.ClearShaderMessages(oldShader);
  529. var newFileContents = FileUtilities.WriteShaderGraphToDisk(path, graphObject.graph);
  530. if (newFileContents != null)
  531. {
  532. saved = true;
  533. m_LastSerializedFileContents = newFileContents;
  534. AssetDatabase.ImportAsset(path);
  535. }
  536. OnSaveGraph(path);
  537. hasUnsavedChanges = false;
  538. }
  539. UpdateTitle();
  540. return saved;
  541. }
  542. void OnSaveGraph(string path)
  543. {
  544. if (GraphData.onSaveGraph == null)
  545. return;
  546. if (graphObject.graph.isSubGraph)
  547. return;
  548. var shader = AssetDatabase.LoadAssetAtPath<Shader>(path);
  549. if (shader == null)
  550. return;
  551. foreach (var target in graphObject.graph.activeTargets)
  552. {
  553. GraphData.onSaveGraph(shader, target.saveContext);
  554. }
  555. }
  556. public void SaveAs()
  557. {
  558. SaveAsImplementation(true);
  559. }
  560. // returns the asset path the file was saved to, or NULL if nothing was saved
  561. string SaveAsImplementation(bool openWhenSaved)
  562. {
  563. string savedFilePath = null;
  564. if (selectedGuid != null && graphObject?.graph != null)
  565. {
  566. var oldFilePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  567. if (string.IsNullOrEmpty(oldFilePath) || graphObject == null)
  568. return null;
  569. // The asset's name needs to be removed from the path, otherwise SaveFilePanel assumes it's a folder
  570. string oldDirectory = Path.GetDirectoryName(oldFilePath);
  571. var extension = graphObject.graph.isSubGraph ? ShaderSubGraphImporter.Extension : ShaderGraphImporter.Extension;
  572. var newFilePath = EditorUtility.SaveFilePanelInProject("Save Graph As...", Path.GetFileNameWithoutExtension(oldFilePath), extension, "", oldDirectory);
  573. newFilePath = newFilePath.Replace(Application.dataPath, "Assets");
  574. if (newFilePath != oldFilePath)
  575. {
  576. if (!string.IsNullOrEmpty(newFilePath))
  577. {
  578. // If the newPath already exists, we are overwriting an existing file, and could be creating recursions. Let's check.
  579. if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(newFilePath, graphObject.graph.GetNodes<SubGraphNode>(), "Save As"))
  580. return null;
  581. bool success = (FileUtilities.WriteShaderGraphToDisk(newFilePath, graphObject.graph) != null);
  582. AssetDatabase.ImportAsset(newFilePath);
  583. if (success)
  584. {
  585. if (openWhenSaved)
  586. ShaderGraphImporterEditor.ShowGraphEditWindow(newFilePath);
  587. OnSaveGraph(newFilePath);
  588. savedFilePath = newFilePath;
  589. }
  590. }
  591. }
  592. else
  593. {
  594. // saving to the current path
  595. if (SaveAsset())
  596. {
  597. graphObject.isDirty = false;
  598. savedFilePath = oldFilePath;
  599. }
  600. }
  601. }
  602. return savedFilePath;
  603. }
  604. public void ToSubGraph()
  605. {
  606. var graphView = graphEditorView.graphView;
  607. string path;
  608. string sessionStateResult = SessionState.GetString(k_PrevSubGraphPathKey, k_PrevSubGraphPathDefaultValue);
  609. string pathToOriginSG = Path.GetDirectoryName(AssetDatabase.GUIDToAssetPath(selectedGuid));
  610. if (!sessionStateResult.Equals(k_PrevSubGraphPathDefaultValue))
  611. {
  612. path = sessionStateResult;
  613. }
  614. else
  615. {
  616. path = pathToOriginSG;
  617. }
  618. path = EditorUtility.SaveFilePanelInProject("Save Sub Graph", "New Shader Sub Graph", ShaderSubGraphImporter.Extension, "", path);
  619. path = path.Replace(Application.dataPath, "Assets");
  620. // Friendly warning that the user is generating a subgraph that would overwrite the one they are currently working on.
  621. if (AssetDatabase.AssetPathToGUID(path) == selectedGuid)
  622. {
  623. 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"))
  624. {
  625. path = "";
  626. }
  627. }
  628. if (path.Length == 0)
  629. return;
  630. 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();
  631. // Convert To Subgraph could create recursive reference loops if the target path already exists
  632. // Let's check for that here
  633. if (!string.IsNullOrEmpty(path))
  634. {
  635. if (GraphUtil.CheckForRecursiveDependencyOnPendingSave(path, nodes.OfType<SubGraphNode>(), "Convert To SubGraph"))
  636. return;
  637. }
  638. graphObject.RegisterCompleteObjectUndo("Convert To Subgraph");
  639. var bounds = Rect.MinMaxRect(float.PositiveInfinity, float.PositiveInfinity, float.NegativeInfinity, float.NegativeInfinity);
  640. foreach (var node in nodes)
  641. {
  642. var center = node.drawState.position.center;
  643. bounds = Rect.MinMaxRect(
  644. Mathf.Min(bounds.xMin, center.x),
  645. Mathf.Min(bounds.yMin, center.y),
  646. Mathf.Max(bounds.xMax, center.x),
  647. Mathf.Max(bounds.yMax, center.y));
  648. }
  649. var middle = bounds.center;
  650. bounds.center = Vector2.zero;
  651. // Collect graph inputs
  652. var graphInputs = graphView.selection.OfType<SGBlackboardField>().Select(x => x.userData as ShaderInput);
  653. var categories = graphView.selection.OfType<SGBlackboardCategory>().Select(x => x.userData as CategoryData);
  654. // Collect the property nodes and get the corresponding properties
  655. var propertyNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is PropertyNode)).Select(x => ((PropertyNode)x.node).property);
  656. var metaProperties = graphView.graph.properties.Where(x => propertyNodes.Contains(x));
  657. // Collect the keyword nodes and get the corresponding keywords
  658. var keywordNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is KeywordNode)).Select(x => ((KeywordNode)x.node).keyword);
  659. var dropdownNodes = graphView.selection.OfType<IShaderNodeView>().Where(x => (x.node is DropdownNode)).Select(x => ((DropdownNode)x.node).dropdown);
  660. var metaKeywords = graphView.graph.keywords.Where(x => keywordNodes.Contains(x));
  661. var metaDropdowns = graphView.graph.dropdowns.Where(x => dropdownNodes.Contains(x));
  662. var copyPasteGraph = new CopyPasteGraph(graphView.selection.OfType<ShaderGroup>().Select(x => x.userData),
  663. nodes,
  664. graphView.selection.OfType<Edge>().Select(x => x.userData as Graphing.Edge),
  665. graphInputs,
  666. categories,
  667. metaProperties,
  668. metaKeywords,
  669. metaDropdowns,
  670. graphView.selection.OfType<StickyNote>().Select(x => x.userData),
  671. true,
  672. false);
  673. // why do we serialize and deserialize only to make copies of everything in the steps below?
  674. // is this just to clear out all non-serialized data?
  675. var deserialized = CopyPasteGraph.FromJson(MultiJson.Serialize(copyPasteGraph), graphView.graph);
  676. if (deserialized == null)
  677. return;
  678. var subGraph = new GraphData { isSubGraph = true, path = "Sub Graphs" };
  679. var subGraphOutputNode = new SubGraphOutputNode();
  680. {
  681. var drawState = subGraphOutputNode.drawState;
  682. drawState.position = new Rect(new Vector2(bounds.xMax + 200f, 0f), drawState.position.size);
  683. subGraphOutputNode.drawState = drawState;
  684. }
  685. subGraph.AddNode(subGraphOutputNode);
  686. subGraph.outputNode = subGraphOutputNode;
  687. // Always copy deserialized keyword inputs
  688. foreach (ShaderKeyword keyword in deserialized.metaKeywords)
  689. {
  690. var copiedInput = (ShaderKeyword)subGraph.AddCopyOfShaderInput(keyword);
  691. // Update the keyword nodes that depends on the copied keyword
  692. var dependentKeywordNodes = deserialized.GetNodes<KeywordNode>().Where(x => x.keyword == keyword);
  693. foreach (var node in dependentKeywordNodes)
  694. {
  695. node.owner = graphView.graph;
  696. node.keyword = copiedInput;
  697. }
  698. }
  699. // Always copy deserialized dropdown inputs
  700. foreach (ShaderDropdown dropdown in deserialized.metaDropdowns)
  701. {
  702. var copiedInput = (ShaderDropdown)subGraph.AddCopyOfShaderInput(dropdown);
  703. // Update the dropdown nodes that depends on the copied dropdown
  704. var dependentDropdownNodes = deserialized.GetNodes<DropdownNode>().Where(x => x.dropdown == dropdown);
  705. foreach (var node in dependentDropdownNodes)
  706. {
  707. node.owner = graphView.graph;
  708. node.dropdown = copiedInput;
  709. }
  710. }
  711. foreach (GroupData groupData in deserialized.groups)
  712. {
  713. subGraph.CreateGroup(groupData);
  714. }
  715. foreach (var node in deserialized.GetNodes<AbstractMaterialNode>())
  716. {
  717. var drawState = node.drawState;
  718. drawState.position = new Rect(drawState.position.position - middle, drawState.position.size);
  719. node.drawState = drawState;
  720. // Checking if the group guid is also being copied.
  721. // If not then nullify that guid
  722. if (node.group != null && !subGraph.groups.Contains(node.group))
  723. {
  724. node.group = null;
  725. }
  726. subGraph.AddNode(node);
  727. }
  728. foreach (var note in deserialized.stickyNotes)
  729. {
  730. if (note.group != null && !subGraph.groups.Contains(note.group))
  731. {
  732. note.group = null;
  733. }
  734. subGraph.AddStickyNote(note);
  735. }
  736. // figure out what needs remapping
  737. var externalOutputSlots = new List<Graphing.Edge>();
  738. var externalInputSlots = new List<Graphing.Edge>();
  739. var passthroughSlots = new List<Graphing.Edge>();
  740. foreach (var edge in deserialized.edges)
  741. {
  742. var outputSlot = edge.outputSlot;
  743. var inputSlot = edge.inputSlot;
  744. var outputSlotExistsInSubgraph = subGraph.ContainsNode(outputSlot.node);
  745. var inputSlotExistsInSubgraph = subGraph.ContainsNode(inputSlot.node);
  746. // pasting nice internal links!
  747. if (outputSlotExistsInSubgraph && inputSlotExistsInSubgraph)
  748. {
  749. subGraph.Connect(outputSlot, inputSlot);
  750. }
  751. // one edge needs to go to outside world
  752. else if (outputSlotExistsInSubgraph)
  753. {
  754. externalInputSlots.Add(edge);
  755. }
  756. else if (inputSlotExistsInSubgraph)
  757. {
  758. externalOutputSlots.Add(edge);
  759. }
  760. else
  761. {
  762. externalInputSlots.Add(edge);
  763. externalOutputSlots.Add(edge);
  764. passthroughSlots.Add(edge);
  765. }
  766. }
  767. // Find the unique edges coming INTO the graph
  768. var uniqueIncomingEdges = externalOutputSlots.GroupBy(
  769. edge => edge.outputSlot,
  770. edge => edge,
  771. (key, edges) => new { slotRef = key, edges = edges.ToList() });
  772. var externalInputNeedingConnection = new List<KeyValuePair<IEdge, AbstractShaderProperty>>();
  773. var amountOfProps = uniqueIncomingEdges.Count();
  774. const int height = 40;
  775. const int subtractHeight = 20;
  776. var propPos = new Vector2(0, -((amountOfProps / 2) + height) - subtractHeight);
  777. var passthroughSlotRefLookup = new Dictionary<SlotReference, SlotReference>();
  778. var passedInProperties = new Dictionary<AbstractShaderProperty, AbstractShaderProperty>();
  779. foreach (var group in uniqueIncomingEdges)
  780. {
  781. var sr = group.slotRef;
  782. var fromNode = sr.node;
  783. var fromSlot = sr.slot;
  784. var materialGraph = graphObject.graph;
  785. var fromProperty = fromNode is PropertyNode fromPropertyNode
  786. ? materialGraph.properties.FirstOrDefault(p => p == fromPropertyNode.property)
  787. : null;
  788. AbstractShaderProperty prop;
  789. if (fromProperty != null && passedInProperties.TryGetValue(fromProperty, out prop))
  790. {
  791. }
  792. else
  793. {
  794. switch (fromSlot.concreteValueType)
  795. {
  796. case ConcreteSlotValueType.Texture2D:
  797. prop = new Texture2DShaderProperty();
  798. break;
  799. case ConcreteSlotValueType.Texture2DArray:
  800. prop = new Texture2DArrayShaderProperty();
  801. break;
  802. case ConcreteSlotValueType.Texture3D:
  803. prop = new Texture3DShaderProperty();
  804. break;
  805. case ConcreteSlotValueType.Cubemap:
  806. prop = new CubemapShaderProperty();
  807. break;
  808. case ConcreteSlotValueType.Vector4:
  809. prop = new Vector4ShaderProperty();
  810. break;
  811. case ConcreteSlotValueType.Vector3:
  812. prop = new Vector3ShaderProperty();
  813. break;
  814. case ConcreteSlotValueType.Vector2:
  815. prop = new Vector2ShaderProperty();
  816. break;
  817. case ConcreteSlotValueType.Vector1:
  818. prop = new Vector1ShaderProperty();
  819. break;
  820. case ConcreteSlotValueType.Boolean:
  821. prop = new BooleanShaderProperty();
  822. break;
  823. case ConcreteSlotValueType.Matrix2:
  824. prop = new Matrix2ShaderProperty();
  825. break;
  826. case ConcreteSlotValueType.Matrix3:
  827. prop = new Matrix3ShaderProperty();
  828. break;
  829. case ConcreteSlotValueType.Matrix4:
  830. prop = new Matrix4ShaderProperty();
  831. break;
  832. case ConcreteSlotValueType.SamplerState:
  833. prop = new SamplerStateShaderProperty();
  834. break;
  835. case ConcreteSlotValueType.Gradient:
  836. prop = new GradientShaderProperty();
  837. break;
  838. case ConcreteSlotValueType.VirtualTexture:
  839. prop = new VirtualTextureShaderProperty()
  840. {
  841. // also copy the VT settings over from the original property (if there is one)
  842. value = (fromProperty as VirtualTextureShaderProperty)?.value ?? new SerializableVirtualTexture()
  843. };
  844. break;
  845. default:
  846. throw new ArgumentOutOfRangeException();
  847. }
  848. var propName = fromProperty != null
  849. ? fromProperty.displayName
  850. : fromSlot.concreteValueType.ToString();
  851. prop.SetDisplayNameAndSanitizeForGraph(subGraph, propName);
  852. if (fromProperty?.useCustomSlotLabel ?? false)
  853. {
  854. prop.useCustomSlotLabel = true;
  855. prop.customSlotLabel = fromProperty.customSlotLabel;
  856. }
  857. subGraph.AddGraphInput(prop);
  858. if (fromProperty != null)
  859. {
  860. passedInProperties.Add(fromProperty, prop);
  861. }
  862. }
  863. var propNode = new PropertyNode();
  864. {
  865. var drawState = propNode.drawState;
  866. drawState.position = new Rect(new Vector2(bounds.xMin - 300f, 0f) + propPos,
  867. drawState.position.size);
  868. propPos += new Vector2(0, height);
  869. propNode.drawState = drawState;
  870. }
  871. subGraph.AddNode(propNode);
  872. propNode.property = prop;
  873. Vector2 avg = Vector2.zero;
  874. foreach (var edge in group.edges)
  875. {
  876. if (passthroughSlots.Contains(edge) && !passthroughSlotRefLookup.ContainsKey(sr))
  877. {
  878. passthroughSlotRefLookup.Add(sr, new SlotReference(propNode, PropertyNode.OutputSlotId));
  879. }
  880. else
  881. {
  882. subGraph.Connect(
  883. new SlotReference(propNode, PropertyNode.OutputSlotId),
  884. edge.inputSlot);
  885. int i;
  886. var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
  887. for (i = 0; i < inputs.Count; ++i)
  888. {
  889. if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
  890. {
  891. break;
  892. }
  893. }
  894. avg += new Vector2(edge.inputSlot.node.drawState.position.xMin, edge.inputSlot.node.drawState.position.center.y + 30f * i);
  895. }
  896. //we collapse input properties so dont add edges that are already being added
  897. if (!externalInputNeedingConnection.Any(x => x.Key.outputSlot.slot == edge.outputSlot.slot && x.Value == prop))
  898. {
  899. externalInputNeedingConnection.Add(new KeyValuePair<IEdge, AbstractShaderProperty>(edge, prop));
  900. }
  901. }
  902. avg /= group.edges.Count;
  903. var pos = avg - new Vector2(150f, 0f);
  904. propNode.drawState = new DrawState()
  905. {
  906. position = new Rect(pos, propNode.drawState.position.size),
  907. expanded = propNode.drawState.expanded
  908. };
  909. }
  910. var uniqueOutgoingEdges = externalInputSlots.GroupBy(
  911. edge => edge.outputSlot,
  912. edge => edge,
  913. (key, edges) => new { slot = key, edges = edges.ToList() });
  914. var externalOutputsNeedingConnection = new List<KeyValuePair<IEdge, IEdge>>();
  915. foreach (var group in uniqueOutgoingEdges)
  916. {
  917. var outputNode = subGraph.outputNode as SubGraphOutputNode;
  918. AbstractMaterialNode node = group.edges[0].outputSlot.node;
  919. MaterialSlot slot = node.FindSlot<MaterialSlot>(group.edges[0].outputSlot.slotId);
  920. var slotId = outputNode.AddSlot(slot.concreteValueType);
  921. var inputSlotRef = new SlotReference(outputNode, slotId);
  922. foreach (var edge in group.edges)
  923. {
  924. var newEdge = subGraph.Connect(passthroughSlotRefLookup.TryGetValue(edge.outputSlot, out SlotReference remap) ? remap : edge.outputSlot, inputSlotRef);
  925. externalOutputsNeedingConnection.Add(new KeyValuePair<IEdge, IEdge>(edge, newEdge));
  926. }
  927. }
  928. if (FileUtilities.WriteShaderGraphToDisk(path, subGraph) != null)
  929. AssetDatabase.ImportAsset(path);
  930. // Store path for next time
  931. if (!pathToOriginSG.Equals(Path.GetDirectoryName(path)))
  932. {
  933. SessionState.SetString(k_PrevSubGraphPathKey, Path.GetDirectoryName(path));
  934. }
  935. else
  936. {
  937. // Or continue to make it so that next time it will open up in the converted-from SG's directory
  938. SessionState.EraseString(k_PrevSubGraphPathKey);
  939. }
  940. var loadedSubGraph = AssetDatabase.LoadAssetAtPath(path, typeof(SubGraphAsset)) as SubGraphAsset;
  941. if (loadedSubGraph == null)
  942. return;
  943. var subGraphNode = new SubGraphNode();
  944. var ds = subGraphNode.drawState;
  945. ds.position = new Rect(middle - new Vector2(100f, 150f), Vector2.zero);
  946. subGraphNode.drawState = ds;
  947. // Add the subgraph into the group if the nodes was all in the same group group
  948. var firstNode = copyPasteGraph.GetNodes<AbstractMaterialNode>().FirstOrDefault();
  949. if (firstNode != null && copyPasteGraph.GetNodes<AbstractMaterialNode>().All(x => x.group == firstNode.group))
  950. {
  951. subGraphNode.group = firstNode.group;
  952. }
  953. subGraphNode.asset = loadedSubGraph;
  954. graphObject.graph.AddNode(subGraphNode);
  955. foreach (var edgeMap in externalInputNeedingConnection)
  956. {
  957. graphObject.graph.Connect(edgeMap.Key.outputSlot, new SlotReference(subGraphNode, edgeMap.Value.guid.GetHashCode()));
  958. }
  959. foreach (var edgeMap in externalOutputsNeedingConnection)
  960. {
  961. graphObject.graph.Connect(new SlotReference(subGraphNode, edgeMap.Value.inputSlot.slotId), edgeMap.Key.inputSlot);
  962. }
  963. graphObject.graph.RemoveElements(
  964. graphView.selection.OfType<IShaderNodeView>().Select(x => x.node).Where(x => !(x is PropertyNode || x is SubGraphOutputNode) && x.allowedInSubGraph).ToArray(),
  965. new IEdge[] { },
  966. new GroupData[] { },
  967. graphView.selection.OfType<StickyNote>().Select(x => x.userData).ToArray());
  968. List<GraphElement> moved = new List<GraphElement>();
  969. foreach (var nodeView in graphView.selection.OfType<IShaderNodeView>())
  970. {
  971. var node = nodeView.node;
  972. if (graphView.graph.removedNodes.Contains(node) || node is SubGraphOutputNode)
  973. {
  974. continue;
  975. }
  976. var edges = graphView.graph.GetEdges(node);
  977. int numEdges = edges.Count();
  978. if (numEdges == 0)
  979. {
  980. graphView.graph.RemoveNode(node);
  981. }
  982. else if (numEdges == 1 && edges.First().inputSlot.node != node) //its an output edge
  983. {
  984. var edge = edges.First();
  985. int i;
  986. var inputs = edge.inputSlot.node.GetInputSlots<MaterialSlot>().ToList();
  987. for (i = 0; i < inputs.Count; ++i)
  988. {
  989. if (inputs[i].slotReference.slotId == edge.inputSlot.slotId)
  990. {
  991. break;
  992. }
  993. }
  994. node.drawState = new DrawState()
  995. {
  996. 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),
  997. expanded = node.drawState.expanded
  998. };
  999. (nodeView as GraphElement).SetPosition(node.drawState.position);
  1000. }
  1001. }
  1002. graphObject.graph.ValidateGraph();
  1003. }
  1004. public void Initialize(MaterialGraphEditWindow other)
  1005. {
  1006. // create a new window that copies the entire editor state of an existing window
  1007. // this function is used to "reopen" an editor window that is closing, but where the user has canceled the close
  1008. // for example, if the graph of a closing window was dirty, but could not be saved
  1009. try
  1010. {
  1011. selectedGuid = other.selectedGuid;
  1012. graphObject = CreateInstance<GraphObject>();
  1013. graphObject.hideFlags = HideFlags.HideAndDontSave;
  1014. graphObject.graph = other.graphObject.graph;
  1015. graphObject.graph.messageManager = this.messageManager;
  1016. UpdateTitle();
  1017. Repaint();
  1018. }
  1019. catch (Exception e)
  1020. {
  1021. Debug.LogException(e);
  1022. m_HasError = true;
  1023. m_GraphEditorView = null;
  1024. graphObject = null;
  1025. throw;
  1026. }
  1027. }
  1028. private static readonly ProfilerMarker GraphLoadMarker = new ProfilerMarker("GraphLoad");
  1029. private static readonly ProfilerMarker CreateGraphEditorViewMarker = new ProfilerMarker("CreateGraphEditorView");
  1030. public void Initialize(string assetGuid)
  1031. {
  1032. try
  1033. {
  1034. m_ColorSpace = PlayerSettings.colorSpace;
  1035. m_RenderPipelineAsset = GraphicsSettings.currentRenderPipeline;
  1036. var asset = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(assetGuid));
  1037. if (asset == null)
  1038. return;
  1039. if (!EditorUtility.IsPersistent(asset))
  1040. return;
  1041. if (selectedGuid == assetGuid)
  1042. return;
  1043. var path = AssetDatabase.GetAssetPath(asset);
  1044. var extension = Path.GetExtension(path);
  1045. if (extension == null)
  1046. return;
  1047. // Path.GetExtension returns the extension prefixed with ".", so we remove it. We force lower case such that
  1048. // the comparison will be case-insensitive.
  1049. extension = extension.Substring(1).ToLowerInvariant();
  1050. bool isSubGraph;
  1051. switch (extension)
  1052. {
  1053. case ShaderGraphImporter.Extension:
  1054. isSubGraph = false;
  1055. break;
  1056. case ShaderSubGraphImporter.Extension:
  1057. isSubGraph = true;
  1058. break;
  1059. default:
  1060. return;
  1061. }
  1062. selectedGuid = assetGuid;
  1063. string graphName = Path.GetFileNameWithoutExtension(path);
  1064. using (GraphLoadMarker.Auto())
  1065. {
  1066. m_LastSerializedFileContents = File.ReadAllText(path, Encoding.UTF8);
  1067. graphObject = CreateInstance<GraphObject>();
  1068. graphObject.hideFlags = HideFlags.HideAndDontSave;
  1069. graphObject.graph = new GraphData
  1070. {
  1071. assetGuid = assetGuid,
  1072. isSubGraph = isSubGraph,
  1073. messageManager = messageManager
  1074. };
  1075. MultiJson.Deserialize(graphObject.graph, m_LastSerializedFileContents);
  1076. graphObject.graph.OnEnable();
  1077. graphObject.graph.ValidateGraph();
  1078. }
  1079. using (CreateGraphEditorViewMarker.Auto())
  1080. {
  1081. graphEditorView = new GraphEditorView(this, m_GraphObject.graph, messageManager, graphName)
  1082. {
  1083. viewDataKey = selectedGuid,
  1084. };
  1085. }
  1086. UpdateTitle();
  1087. Repaint();
  1088. }
  1089. catch (Exception)
  1090. {
  1091. m_HasError = true;
  1092. m_GraphEditorView = null;
  1093. graphObject = null;
  1094. throw;
  1095. }
  1096. }
  1097. // returns contents of the asset file, or null if any exception occurred
  1098. private string ReadAssetFile()
  1099. {
  1100. var filePath = AssetDatabase.GUIDToAssetPath(selectedGuid);
  1101. return FileUtilities.SafeReadAllText(filePath);
  1102. }
  1103. // 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)
  1104. // returns false when the user wants to cancel closing the window or application
  1105. internal bool PromptSaveIfDirtyOnQuit()
  1106. {
  1107. // only bother unless we've actually got data to preserve
  1108. if (graphObject?.graph != null)
  1109. {
  1110. // if the asset has been deleted, ask the user what to do
  1111. if (!AssetFileExists())
  1112. return DisplayDeletedFromDiskDialog(false);
  1113. // If there are unsaved modifications, ask the user what to do.
  1114. // If the editor has already handled this check we'll no longer have unsaved changes
  1115. // (either they saved or they discarded, both of which will set hasUnsavedChanges to false).
  1116. if (hasUnsavedChanges)
  1117. {
  1118. int option = EditorUtility.DisplayDialogComplex(
  1119. "Shader Graph Has Been Modified",
  1120. GetSaveChangesMessage(),
  1121. "Save", "Cancel", "Discard Changes");
  1122. if (option == 0) // save
  1123. {
  1124. return SaveAsset();
  1125. }
  1126. else if (option == 1) // cancel (or escape/close dialog)
  1127. {
  1128. return false;
  1129. }
  1130. else if (option == 2) // discard
  1131. {
  1132. return true;
  1133. }
  1134. }
  1135. }
  1136. return true;
  1137. }
  1138. private string GetSaveChangesMessage()
  1139. {
  1140. return "Do you want to save the changes you made in the Shader Graph?\n\n" +
  1141. AssetDatabase.GUIDToAssetPath(selectedGuid) +
  1142. "\n\nYour changes will be lost if you don't save them.";
  1143. }
  1144. public override void SaveChanges()
  1145. {
  1146. base.SaveChanges();
  1147. SaveAsset();
  1148. }
  1149. void OnGeometryChanged(GeometryChangedEvent evt)
  1150. {
  1151. if (graphEditorView == null)
  1152. return;
  1153. // this callback is only so we can run post-layout behaviors after the graph loads for the first time
  1154. // we immediately unregister it so it doesn't get called again
  1155. graphEditorView.UnregisterCallback<GeometryChangedEvent>(OnGeometryChanged);
  1156. if (m_FrameAllAfterLayout)
  1157. graphEditorView.graphView.FrameAll();
  1158. m_FrameAllAfterLayout = false;
  1159. }
  1160. }
  1161. }