using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; using UnityEditor.Graphing; using UnityEditor.Graphing.Util; using UnityEditor.ShaderGraph.Drawing.Controls; using UnityEngine.Rendering; using UnityEditor.Experimental.GraphView; using UnityEditor.Rendering; using UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers; using UnityEditor.ShaderGraph.Internal; using UnityEngine.UIElements; using UnityEditor.UIElements; using Node = UnityEditor.Experimental.GraphView.Node; namespace UnityEditor.ShaderGraph.Drawing { sealed class MaterialNodeView : Node, IShaderNodeView, IInspectable { PreviewRenderData m_PreviewRenderData; Image m_PreviewImage; // Remove this after updated to the correct API call has landed in trunk. ------------ VisualElement m_TitleContainer; new VisualElement m_ButtonContainer; VisualElement m_PreviewContainer; VisualElement m_PreviewFiller; VisualElement m_ControlItems; VisualElement m_ControlsDivider; VisualElement m_DropdownItems; VisualElement m_DropdownsDivider; IEdgeConnectorListener m_ConnectorListener; MaterialGraphView m_GraphView; public string inspectorTitle => $"{node.name} (Node)"; public void Initialize(AbstractMaterialNode inNode, PreviewManager previewManager, IEdgeConnectorListener connectorListener, MaterialGraphView graphView) { styleSheets.Add(Resources.Load("Styles/MaterialNodeView")); styleSheets.Add(Resources.Load($"Styles/ColorMode")); AddToClassList("MaterialNode"); if (inNode == null) return; var contents = this.Q("contents"); m_GraphView = graphView; mainContainer.style.overflow = StyleKeyword.None; // Override explicit style set in base class m_ConnectorListener = connectorListener; node = inNode; viewDataKey = node.objectId; UpdateTitle(); // Add disabled overlay Add(new VisualElement() { name = "disabledOverlay", pickingMode = PickingMode.Ignore }); // Add controls container var controlsContainer = new VisualElement { name = "controls" }; { m_ControlsDivider = new VisualElement { name = "divider" }; m_ControlsDivider.AddToClassList("horizontal"); controlsContainer.Add(m_ControlsDivider); m_ControlItems = new VisualElement { name = "items" }; controlsContainer.Add(m_ControlItems); // Instantiate control views from node foreach (var propertyInfo in node.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) foreach (IControlAttribute attribute in propertyInfo.GetCustomAttributes(typeof(IControlAttribute), false)) m_ControlItems.Add(attribute.InstantiateControl(node, propertyInfo)); } if (m_ControlItems.childCount > 0) contents.Add(controlsContainer); // Add dropdowns container var dropdownContainer = new VisualElement { name = "dropdowns" }; { m_DropdownsDivider = new VisualElement { name = "divider" }; m_DropdownsDivider.AddToClassList("horizontal"); dropdownContainer.Add(m_DropdownsDivider); m_DropdownItems = new VisualElement { name = "items" }; dropdownContainer.Add(m_DropdownItems); UpdateDropdownEntries(); } if (m_DropdownItems.childCount > 0) contents.Add(dropdownContainer); if (node.hasPreview) { // Add actual preview which floats on top of the node m_PreviewContainer = new VisualElement { name = "previewContainer", style = { overflow = Overflow.Hidden }, pickingMode = PickingMode.Ignore }; m_PreviewImage = new Image { name = "preview", pickingMode = PickingMode.Ignore, image = Texture2D.whiteTexture, }; { // Add preview collapse button on top of preview var collapsePreviewButton = new VisualElement { name = "collapse" }; collapsePreviewButton.Add(new VisualElement { name = "icon" }); collapsePreviewButton.AddManipulator(new Clickable(() => { SetPreviewExpandedStateOnSelection(false); })); m_PreviewImage.Add(collapsePreviewButton); } m_PreviewContainer.Add(m_PreviewImage); // Hook up preview image to preview manager m_PreviewRenderData = previewManager.GetPreviewRenderData(inNode); m_PreviewRenderData.onPreviewChanged += UpdatePreviewTexture; UpdatePreviewTexture(); // Add fake preview which pads out the node to provide space for the floating preview m_PreviewFiller = new VisualElement { name = "previewFiller" }; m_PreviewFiller.AddToClassList("expanded"); { var previewDivider = new VisualElement { name = "divider" }; previewDivider.AddToClassList("horizontal"); m_PreviewFiller.Add(previewDivider); var expandPreviewButton = new VisualElement { name = "expand" }; expandPreviewButton.Add(new VisualElement { name = "icon" }); expandPreviewButton.AddManipulator(new Clickable(() => { SetPreviewExpandedStateOnSelection(true); })); m_PreviewFiller.Add(expandPreviewButton); } contents.Add(m_PreviewFiller); UpdatePreviewExpandedState(node.previewExpanded); } base.expanded = node.drawState.expanded; AddSlots(node.GetSlots()); if (node is SubGraphNode) { RegisterCallback(OnSubGraphDoubleClick); } m_TitleContainer = this.Q("title"); if (node is BlockNode blockData) { AddToClassList("blockData"); m_TitleContainer.RemoveFromHierarchy(); } else { SetPosition(new Rect(node.drawState.position.x, node.drawState.position.y, 0, 0)); } // Update active state SetActive(node.isActive); // Register OnMouseHover callbacks for node highlighting RegisterCallback(OnMouseHover); RegisterCallback(OnMouseHover); ShaderGraphPreferences.onAllowDeprecatedChanged += UpdateTitle; } public bool FindPort(SlotReference slotRef, out ShaderPort port) { port = inputContainer.Query().ToList() .Concat(outputContainer.Query().ToList()) .First(p => p.slot.slotReference.Equals(slotRef)); return port != null; } public void AttachMessage(string errString, ShaderCompilerMessageSeverity severity) { ClearMessage(); IconBadge badge; if (severity == ShaderCompilerMessageSeverity.Error) { badge = IconBadge.CreateError(errString); } else { badge = IconBadge.CreateComment(errString); } Add(badge); if (node is BlockNode) { FindPort(node.GetSlotReference(0), out var port); badge.AttachTo(port.parent, SpriteAlignment.RightCenter); } else { badge.AttachTo(m_TitleContainer, SpriteAlignment.RightCenter); } } public void SetActive(bool state) { // Setup var disabledString = "disabled"; var portDisabledString = "inactive"; if (!state) { // Add elements to disabled class list AddToClassList(disabledString); var inputPorts = inputContainer.Query().ToList(); foreach (var port in inputPorts) { port.AddToClassList(portDisabledString); } var outputPorts = outputContainer.Query().ToList(); foreach (var port in outputPorts) { port.AddToClassList(portDisabledString); } } else { // Remove elements from disabled class list RemoveFromClassList(disabledString); var inputPorts = inputContainer.Query().ToList(); foreach (var port in inputPorts) { port.RemoveFromClassList(portDisabledString); } var outputPorts = outputContainer.Query().ToList(); foreach (var port in outputPorts) { port.RemoveFromClassList(portDisabledString); } } } public void ClearMessage() { var badge = this.Q(); badge?.Detach(); badge?.RemoveFromHierarchy(); } public void UpdateDropdownEntries() { if (node is SubGraphNode subGraphNode && subGraphNode.asset != null) { m_DropdownItems.Clear(); var dropdowns = subGraphNode.asset.dropdowns; foreach (var dropdown in dropdowns) { if (dropdown.isExposed) { var name = subGraphNode.GetDropdownEntryName(dropdown.referenceName); if (!dropdown.ContainsEntry(name)) { name = dropdown.entryName; subGraphNode.SetDropdownEntryName(dropdown.referenceName, name); } var field = new PopupField(dropdown.entries.Select(x => x.displayName).ToList(), name); field.RegisterValueChangedCallback(evt => { subGraphNode.owner.owner.RegisterCompleteObjectUndo("Change Dropdown Value"); subGraphNode.SetDropdownEntryName(dropdown.referenceName, field.value); subGraphNode.Dirty(ModificationScope.Topological); }); m_DropdownItems.Add(new PropertyRow(new Label(dropdown.displayName)), (row) => { row.styleSheets.Add(Resources.Load("Styles/PropertyRow")); row.Add(field); }); } } } } public VisualElement colorElement { get { return this; } } static readonly StyleColor noColor = new StyleColor(StyleKeyword.Null); public void SetColor(Color color) { m_TitleContainer.style.borderBottomColor = color; } public void ResetColor() { m_TitleContainer.style.borderBottomColor = noColor; } public Color GetColor() { return m_TitleContainer.resolvedStyle.borderBottomColor; } void OnSubGraphDoubleClick(MouseDownEvent evt) { if (evt.clickCount == 2 && evt.button == 0) { SubGraphNode subgraphNode = node as SubGraphNode; var path = AssetDatabase.GUIDToAssetPath(subgraphNode.subGraphGuid); ShaderGraphImporterEditor.ShowGraphEditWindow(path); } } public Node gvNode => this; [Inspectable("Node", null)] public AbstractMaterialNode node { get; private set; } public override bool expanded { get => base.expanded; set { if (base.expanded == value) return; base.expanded = value; if (node.drawState.expanded != value) { var ds = node.drawState; ds.expanded = value; node.drawState = ds; } foreach (var inputPort in inputContainer.Query().ToList()) { inputPort.parent.style.visibility = inputPort.style.visibility; } RefreshExpandedState(); // Necessary b/c we can't override enough Node.cs functions to update only what's needed } } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { if (evt.target is Node) { var canViewShader = node.hasPreview || node is SubGraphOutputNode; evt.menu.AppendAction("Copy Shader", CopyToClipboard, _ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden, GenerationMode.ForReals); evt.menu.AppendAction("Show Generated Code", ShowGeneratedCode, _ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden, GenerationMode.ForReals); if (Unsupported.IsDeveloperMode()) { evt.menu.AppendAction("Show Preview Code", ShowGeneratedCode, _ => canViewShader ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Hidden, GenerationMode.Preview); } } base.BuildContextualMenu(evt); } void CopyToClipboard(DropdownMenuAction action) { GUIUtility.systemCopyBuffer = ConvertToShader((GenerationMode)action.userData); } public string SanitizeName(string name) { return new string(name.Where(c => !Char.IsWhiteSpace(c)).ToArray()); } public void ShowGeneratedCode(DropdownMenuAction action) { string name = GetFirstAncestorOfType().assetName; var mode = (GenerationMode)action.userData; string path = String.Format("Temp/GeneratedFromGraph-{0}-{1}-{2}{3}.shader", SanitizeName(name), SanitizeName(node.name), node.objectId, mode == GenerationMode.Preview ? "-Preview" : ""); if (GraphUtil.WriteToFile(path, ConvertToShader(mode))) GraphUtil.OpenFile(path); } string ConvertToShader(GenerationMode mode) { var generator = new Generator(node.owner, node, mode, node.name, null); return generator.generatedShader; } void SetNodesAsDirty() { var editorView = GetFirstAncestorOfType(); var nodeList = m_GraphView.Query().ToList(); editorView.colorManager.SetNodesDirty(nodeList); } void UpdateNodeViews() { var editorView = GetFirstAncestorOfType(); var nodeList = m_GraphView.Query().ToList(); editorView.colorManager.UpdateNodeViews(nodeList); } public object GetObjectToInspect() { return node; } public void SupplyDataToPropertyDrawer(IPropertyDrawer propertyDrawer, Action inspectorUpdateDelegate) { if (propertyDrawer is IGetNodePropertyDrawerPropertyData nodePropertyDrawer) { nodePropertyDrawer.GetPropertyData(SetNodesAsDirty, UpdateNodeViews); } } private void SetSelfSelected() { m_GraphView.ClearSelection(); m_GraphView.AddToSelection(this); } protected override void ToggleCollapse() { node.owner.owner.RegisterCompleteObjectUndo(!expanded ? "Expand Nodes" : "Collapse Nodes"); expanded = !expanded; // If selected, expand/collapse the other applicable nodes that are also selected if (selected) { m_GraphView.SetNodeExpandedForSelectedNodes(expanded, false); } } void SetPreviewExpandedStateOnSelection(bool state) { // If selected, expand/collapse the other applicable nodes that are also selected if (selected) { m_GraphView.SetPreviewExpandedForSelectedNodes(state); } else { node.owner.owner.RegisterCompleteObjectUndo(state ? "Expand Previews" : "Collapse Previews"); node.previewExpanded = state; } } public bool CanToggleNodeExpanded() { return !(node is BlockNode) && m_CollapseButton.enabledInHierarchy; } void UpdatePreviewExpandedState(bool expanded) { node.previewExpanded = expanded; if (m_PreviewFiller == null) return; if (expanded) { if (m_PreviewContainer.parent != this) { Add(m_PreviewContainer); m_PreviewContainer.PlaceBehind(this.Q("selection-border")); } m_PreviewFiller.AddToClassList("expanded"); m_PreviewFiller.RemoveFromClassList("collapsed"); } else { if (m_PreviewContainer.parent == m_PreviewFiller) { m_PreviewContainer.RemoveFromHierarchy(); } m_PreviewFiller.RemoveFromClassList("expanded"); m_PreviewFiller.AddToClassList("collapsed"); } UpdatePreviewTexture(); } void UpdateTitle() { if (node is SubGraphNode subGraphNode && subGraphNode.asset != null) title = subGraphNode.asset.name; else { if (node.sgVersion < node.latestVersion) { if (node is IHasCustomDeprecationMessage customDeprecationMessage) { title = customDeprecationMessage.GetCustomDeprecationLabel(); } else if (ShaderGraphPreferences.allowDeprecatedBehaviors) { title = node.name + $" (Deprecated V{node.sgVersion})"; } else { title = node.name + $" (Deprecated)"; } } else { title = node.name; } } } void UpdateShaderPortsForSlots(bool inputSlots, List allSlots, ShaderPort[] slotShaderPorts) { VisualElement portContainer = inputSlots ? inputContainer : outputContainer; var existingPorts = portContainer.Query().ToList(); foreach (ShaderPort shaderPort in existingPorts) { var currentSlotId = shaderPort.slot.id; int newSlotIndex = allSlots.FindIndex(s => s.id == currentSlotId); if (newSlotIndex < 0) { // slot doesn't exist anymore, remove it if (inputSlots) portContainer.Remove(shaderPort.parent); // remove parent (includes the InputView) else portContainer.Remove(shaderPort); } else { var newSlot = allSlots[newSlotIndex]; slotShaderPorts[newSlotIndex] = shaderPort; // these should probably be in an UpdateShaderPort(shaderPort, newSlot) function shaderPort.slot = newSlot; shaderPort.portName = newSlot.displayName; if (inputSlots) // input slots also have to update the InputView UpdatePortInputView(shaderPort); } } } public void OnModified(ModificationScope scope) { UpdateTitle(); SetActive(node.isActive); if (node.hasPreview) UpdatePreviewExpandedState(node.previewExpanded); base.expanded = node.drawState.expanded; switch (scope) { // Update slots to match node modification case ModificationScope.Topological: { var slots = node.GetSlots().ToList(); // going to record the corresponding ShaderPort to each slot, so we can order them later ShaderPort[] slotShaderPorts = new ShaderPort[slots.Count]; // update existing input and output ports UpdateShaderPortsForSlots(true, slots, slotShaderPorts); UpdateShaderPortsForSlots(false, slots, slotShaderPorts); // check if there are any new slots that must create new ports for (int i = 0; i < slots.Count; i++) { if (slotShaderPorts[i] == null) slotShaderPorts[i] = AddShaderPortForSlot(slots[i]); } // make sure they are in the right order // by bringing each port to front in declaration order // note that this sorts input and output containers at the same time foreach (var shaderPort in slotShaderPorts) { if (shaderPort != null) { if (shaderPort.slot.isInputSlot) shaderPort.parent.BringToFront(); else shaderPort.BringToFront(); } } break; } } RefreshExpandedState(); // Necessary b/c we can't override enough Node.cs functions to update only what's needed foreach (var listener in m_ControlItems.Children().OfType()) { if (listener != null) listener.OnNodeModified(scope); } } ShaderPort AddShaderPortForSlot(MaterialSlot slot) { if (slot.hidden) return null; ShaderPort port = ShaderPort.Create(slot, m_ConnectorListener); if (slot.isOutputSlot) { outputContainer.Add(port); } else { var portContainer = new VisualElement(); portContainer.style.flexDirection = FlexDirection.Row; var portInputView = new PortInputView(slot) { style = { position = Position.Absolute } }; portContainer.Add(portInputView); portContainer.Add(port); inputContainer.Add(portContainer); // Update active state if (node.isActive) { portInputView.RemoveFromClassList("disabled"); } else { portInputView.AddToClassList("disabled"); } } port.OnDisconnect = OnEdgeDisconnected; return port; } void AddSlots(IEnumerable slots) { foreach (var slot in slots) AddShaderPortForSlot(slot); // Make sure the visuals are properly updated to reflect port list RefreshPorts(); } void OnEdgeDisconnected(Port obj) { RefreshExpandedState(); } static bool GetPortInputView(ShaderPort port, out PortInputView view) { view = port.parent.Q(); return view != null; } public void UpdatePortInputTypes() { var portList = inputContainer.Query().ToList(); portList.AddRange(outputContainer.Query().ToList()); foreach (var anchor in portList) { var slot = anchor.slot; anchor.portName = slot.displayName; anchor.visualClass = slot.concreteValueType.ToClassName(); if (GetPortInputView(anchor, out var portInputView)) { portInputView.UpdateSlotType(); UpdatePortInputVisibility(portInputView, anchor); } } foreach (var control in m_ControlItems.Children()) { if (control is AbstractMaterialNodeModificationListener listener) listener.OnNodeModified(ModificationScope.Graph); } } void UpdatePortInputView(ShaderPort port) { if (GetPortInputView(port, out var portInputView)) { portInputView.UpdateSlot(port.slot); UpdatePortInputVisibility(portInputView, port); } } void UpdatePortInputVisibility(PortInputView portInputView, ShaderPort port) { SetElementVisible(portInputView, !port.slot.isConnected); port.parent.style.visibility = port.style.visibility; portInputView.MarkDirtyRepaint(); } void SetElementVisible(VisualElement element, bool isVisible) { const string k_HiddenClassList = "hidden"; if (isVisible) { // Restore default value for visibility by setting it to StyleKeyword.Null. // Setting it to Visibility.Visible would make it visible even if parent is hidden. element.style.visibility = StyleKeyword.Null; element.RemoveFromClassList(k_HiddenClassList); } else { element.style.visibility = Visibility.Hidden; element.AddToClassList(k_HiddenClassList); } } SGBlackboardRow GetAssociatedBlackboardRow() { var graphEditorView = GetFirstAncestorOfType(); if (graphEditorView == null) return null; var blackboardController = graphEditorView.blackboardController; if (blackboardController == null) return null; if (node is KeywordNode keywordNode) { return blackboardController.GetBlackboardRow(keywordNode.keyword); } if (node is DropdownNode dropdownNode) { return blackboardController.GetBlackboardRow(dropdownNode.dropdown); } return null; } void OnMouseHover(EventBase evt) { // Keyword/Dropdown nodes should be highlighted when Blackboard entry is hovered // TODO: Move to new NodeView type when keyword node has unique style var blackboardRow = GetAssociatedBlackboardRow(); if (blackboardRow != null) { if (evt.eventTypeId == MouseEnterEvent.TypeId()) { blackboardRow.AddToClassList("hovered"); } else { blackboardRow.RemoveFromClassList("hovered"); } } } void UpdatePreviewTexture() { if (m_PreviewRenderData.texture == null || !node.previewExpanded) { m_PreviewImage.visible = false; m_PreviewImage.image = Texture2D.blackTexture; } else { m_PreviewImage.visible = true; m_PreviewImage.AddToClassList("visible"); m_PreviewImage.RemoveFromClassList("hidden"); if (m_PreviewImage.image != m_PreviewRenderData.texture) m_PreviewImage.image = m_PreviewRenderData.texture; else m_PreviewImage.MarkDirtyRepaint(); if (m_PreviewRenderData.shaderData.isOutOfDate) m_PreviewImage.tintColor = new Color(1.0f, 1.0f, 1.0f, 0.3f); else m_PreviewImage.tintColor = Color.white; } } public void Dispose() { foreach (var portInputView in inputContainer.Query().ToList()) portInputView.Dispose(); var propRow = GetAssociatedBlackboardRow(); // If this node view is deleted, remove highlighting from associated blackboard row if (propRow != null) { propRow.RemoveFromClassList("hovered"); } node = null; userData = null; if (m_PreviewRenderData != null) { m_PreviewRenderData.onPreviewChanged -= UpdatePreviewTexture; m_PreviewRenderData = null; } ShaderGraphPreferences.onAllowDeprecatedChanged -= UpdateTitle; } } }