using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using UnityEditor.Experimental.GraphView; using UnityEngine; using UnityEngine.UIElements; using UnityEngine.Profiling; using PositionType = UnityEngine.UIElements.Position; namespace UnityEditor.VFX.UI { class VFXNodeUI : Node, IControlledElement, ISettableControlledElement, IVFXMovable { bool m_Selected; VFXNodeController m_Controller; readonly List m_Settings = new(); static string UXMLResourceToPackage(string resourcePath) { return VisualEffectAssetEditorUtility.editorResourcesPath + "/" + resourcePath + ".uxml"; } public VFXNodeUI() : base(UXMLResourceToPackage("uxml/VFXNode")) { styleSheets.Add(EditorGUIUtility.Load("StyleSheets/GraphView/Node.uss") as StyleSheet); Initialize(); } public VFXNodeUI(string template) : base(UXMLResourceToPackage(template)) { Initialize(); } public virtual bool superCollapsed => controller.superCollapsed; private VisualElement settingsContainer { get; set; } public override bool expanded { get => base.expanded; set { if (base.expanded == value) return; base.expanded = value; controller.expanded = value; UpdateActivationPortPositionIfAny(); } } public override string title { get => controller?.name; set { } } protected float defaultLabelWidth { get; set; } = DefaultLabelWidth; protected bool hasSettings { get; private set; } Controller IControlledElement.controller => m_Controller; public delegate void SelectionEvent(bool selfSelected); public event SelectionEvent onSelectionDelegate; public void OnMoved() { controller.position = GetPosition().position; } public VFXNodeController controller { get => m_Controller; set { m_Controller?.UnregisterHandler(this); m_Controller = value; OnNewController(); m_Controller?.RegisterHandler(this); } } protected virtual void OnNewController() { if (controller != null) viewDataKey = $"NodeID-{controller.model.GetInstanceID()}"; } public void OnSelectionMouseDown(MouseDownEvent e) { var gv = GetFirstAncestorOfType(); if (IsSelected(gv)) { if (e.actionKey) { Unselect(gv); } } else { Select(gv, e.actionKey); } } void OnFocusIn(FocusInEvent e) { var gv = GetFirstAncestorOfType(); if (!IsSelected(gv)) Select(gv, false); e.StopPropagation(); } void OnPointerEnter(PointerEnterEvent e) { e.StopPropagation(); } void OnPointerLeave(PointerLeaveEvent e) { e.StopPropagation(); } protected virtual void OnPostLayout(GeometryChangedEvent e) { RefreshLayout(); } public override void OnSelected() { base.OnSelected(); m_Selected = true; onSelectionDelegate?.Invoke(m_Selected); } public override void OnUnselected() { m_Selected = false; onSelectionDelegate?.Invoke(m_Selected); base.OnUnselected(); } void Initialize() { this.AddStyleSheetPath("VFXNode"); AddToClassList("VFXNodeUI"); settingsContainer = this.Q("settings"); // Remove useless child element to reduce number of VisualElements this.Q("collapse-button")?.Clear(); RegisterCallback(OnPointerEnter); RegisterCallback(OnPointerLeave); RegisterCallback(OnFocusIn); RegisterCallback(OnPostLayout); } public virtual void OnControllerChanged(ref ControllerChangedEvent e) { if (e.controller == controller) { Profiler.BeginSample(GetType().Name + "::SelfChange()"); SelfChange(); Profiler.EndSample(); } else if (e.controller is VFXDataAnchorController) { RefreshExpandedState(); } } protected virtual bool HasPosition() => true; private void SyncSettings() { Profiler.BeginSample("VFXNodeUI.SyncSettings"); var settings = controller.settings; var graphSettings = controller.model.GetSettings(false, VFXSettingAttribute.VisibleFlags.InGraph).ToArray(); // Remove extra settings foreach (var propertyRM in m_Settings.ToArray()) { if (graphSettings.All(x => string.Compare(x.field.Name, propertyRM.provider.name, StringComparison.OrdinalIgnoreCase) != 0)) { propertyRM.RemoveFromHierarchy(); m_Settings.Remove(propertyRM); } } // Add missing settings for (var i = 0; i < graphSettings.Length; i++) { var vfxSetting = graphSettings[i]; if (m_Settings.All(x => string.Compare(x.provider.name, vfxSetting.name, StringComparison.OrdinalIgnoreCase) != 0)) { var setting = settings.Single(x => string.Compare(x.name, vfxSetting.field.Name, StringComparison.OrdinalIgnoreCase) == 0); var propertyRM = AddSetting(setting); settingsContainer.Insert(i, propertyRM); } } foreach (var propertyRM in m_Settings.ToArray()) { propertyRM.Update(); } hasSettings = m_Settings.Count > 0; if (settingsContainer != null) { if (hasSettings) { RemoveFromClassList("nosettings"); settingsContainer.RemoveFromClassList("nosettings"); } else { AddToClassList("nosettings"); settingsContainer.AddToClassList("nosettings"); } } Profiler.EndSample(); } void SyncAnchors() { Profiler.BeginSample("VFXNodeUI.SyncAnchors"); SyncAnchors(controller.outputPorts, outputContainer, false); SyncAnchors(controller.inputPorts, inputContainer, controller.HasActivationAnchor); Profiler.EndSample(); } void SyncAnchors(ReadOnlyCollection ports, VisualElement container, bool hasActivationPort) { // Check whether resync is needed bool needsResync = false; if (ports.Count != container.childCount) // first check expected number match needsResync = true; else { for (int i = 0; i < ports.Count; ++i) // Then compare expected anchor one by one { VFXDataAnchor anchor = container[i] as VFXDataAnchor; if (ports[i] == null) throw new NullReferenceException("VFXDataAnchorController should not be null at index " + i); if (anchor?.controller != ports[i]) { needsResync = true; break; } } } if (needsResync) { var existingAnchors = container.Children().Cast() .Union(titleContainer.Query().ToList()) .ToDictionary(t => t.controller, t => t); container.Clear(); for (int i = 0; i < ports.Count; ++i) { VFXDataAnchorController portController = ports[i]; if (!existingAnchors.Remove(portController, out var anchor)) anchor = InstantiateDataAnchor(portController, this); // new anchor if (hasActivationPort && i == 1 || !hasActivationPort && i == 0) { anchor.AddToClassList("first"); } else { anchor.RemoveFromClassList("first"); } container.Add(anchor); } // delete no longer used anchors foreach (var anchor in existingAnchors.Values) { GetFirstAncestorOfType()?.RemoveAnchorEdges(anchor); anchor.parent?.Remove(anchor); } } if (hasActivationPort) UpdateActivationPortPositionIfAny(); // Needed to account for expanded state change in case of undo/redo } private void UpdateActivationPortPosition(VFXDataAnchor anchor) { if (anchor.controller.isSubgraphActivation) anchor.AddToClassList("subgraphblock"); titleContainer.AddToClassList("activationslot"); anchor.AddToClassList("activationslot"); AddToClassList("activationslot"); } private bool UpdateActivationPortPositionIfAny() { if (controller.HasActivationAnchor) { var anchorController = controller.inputPorts[0]; var anchor = inputContainer.Children() .Cast() .SingleOrDefault(x => x.controller == anchorController); if (anchor != null) { anchor.RemoveFromHierarchy(); titleContainer.Insert(0, anchor); } else { anchor = titleContainer.Q(); } if (anchor != null) { UpdateActivationPortPosition(anchor); return true; } } return false; } public void ForceUpdate() { SelfChange(); } protected void UpdateCollapse() { if (superCollapsed) { AddToClassList("superCollapsed"); } else { RemoveFromClassList("superCollapsed"); } if (controller.inputPorts.Count == (controller.HasActivationAnchor ? 1 : 0) && controller.outputPorts.Count == 0) { AddToClassList("cannot-expand"); } else { RemoveFromClassList("cannot-expand"); } } public void AssetMoved() { title = controller.title; m_Settings.ForEach(x => x.UpdateGUI(true)); foreach (VFXEditableDataAnchor input in GetPorts(true, false).OfType()) { input.AssetMoved(); } } protected virtual void UpdateTitleUI() { titleContainer .Children() .OfType