388 lines
15 KiB
C#
388 lines
15 KiB
C#
|
using System;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System.Reflection;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.UIElements;
|
||
|
using UnityEditor.Graphing;
|
||
|
using UnityEditor.Experimental.GraphView;
|
||
|
using UnityEditor.ShaderGraph.Drawing.Controls;
|
||
|
using UnityEditor.ShaderGraph.Drawing.Inspector.PropertyDrawers;
|
||
|
using UnityEditor.ShaderGraph.Internal;
|
||
|
using UnityEngine.Assertions;
|
||
|
|
||
|
using ContextualMenuManipulator = UnityEngine.UIElements.ContextualMenuManipulator;
|
||
|
using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>;
|
||
|
|
||
|
namespace UnityEditor.ShaderGraph.Drawing
|
||
|
{
|
||
|
class SGBlackboardField : GraphElement, IInspectable, ISGControlledElement<ShaderInputViewController>
|
||
|
{
|
||
|
static readonly Texture2D k_ExposedIcon = Resources.Load<Texture2D>("GraphView/Nodes/BlackboardFieldExposed");
|
||
|
static readonly string k_UxmlTemplatePath = "UXML/Blackboard/SGBlackboardField";
|
||
|
static readonly string k_StyleSheetPath = "Styles/SGBlackboard";
|
||
|
|
||
|
ShaderInputViewModel m_ViewModel;
|
||
|
|
||
|
ShaderInputViewModel ViewModel
|
||
|
{
|
||
|
get => m_ViewModel;
|
||
|
set => m_ViewModel = value;
|
||
|
}
|
||
|
|
||
|
VisualElement m_ContentItem;
|
||
|
Pill m_Pill;
|
||
|
Label m_TypeLabel;
|
||
|
TextField m_TextField;
|
||
|
internal TextField textField => m_TextField;
|
||
|
|
||
|
Action m_ResetReferenceNameTrigger;
|
||
|
List<Node> m_SelectedNodes = new List<Node>();
|
||
|
|
||
|
public string text
|
||
|
{
|
||
|
get { return m_Pill.text; }
|
||
|
set { m_Pill.text = value; }
|
||
|
}
|
||
|
|
||
|
public string typeText
|
||
|
{
|
||
|
get { return m_TypeLabel.text; }
|
||
|
set { m_TypeLabel.text = value; }
|
||
|
}
|
||
|
|
||
|
public Texture icon
|
||
|
{
|
||
|
get { return m_Pill.icon; }
|
||
|
set { m_Pill.icon = value; }
|
||
|
}
|
||
|
|
||
|
public bool highlighted
|
||
|
{
|
||
|
get { return m_Pill.highlighted; }
|
||
|
set { m_Pill.highlighted = value; }
|
||
|
}
|
||
|
|
||
|
internal SGBlackboardField(ShaderInputViewModel viewModel)
|
||
|
{
|
||
|
ViewModel = viewModel;
|
||
|
// Store ShaderInput in userData object
|
||
|
userData = ViewModel.model;
|
||
|
if (userData == null)
|
||
|
{
|
||
|
AssertHelpers.Fail("Could not initialize blackboard field as shader input was null.");
|
||
|
return;
|
||
|
}
|
||
|
// Store the Model guid as viewDataKey as that is persistent
|
||
|
viewDataKey = ViewModel.model.guid.ToString();
|
||
|
|
||
|
var visualTreeAsset = Resources.Load<VisualTreeAsset>(k_UxmlTemplatePath);
|
||
|
Assert.IsNotNull(visualTreeAsset);
|
||
|
|
||
|
VisualElement mainContainer = visualTreeAsset.Instantiate();
|
||
|
var styleSheet = Resources.Load<StyleSheet>(k_StyleSheetPath);
|
||
|
Assert.IsNotNull(styleSheet);
|
||
|
styleSheets.Add(styleSheet);
|
||
|
|
||
|
mainContainer.AddToClassList("mainContainer");
|
||
|
mainContainer.pickingMode = PickingMode.Ignore;
|
||
|
|
||
|
m_ContentItem = mainContainer.Q("contentItem");
|
||
|
m_Pill = mainContainer.Q<Pill>("pill");
|
||
|
m_TypeLabel = mainContainer.Q<Label>("typeLabel");
|
||
|
m_TextField = mainContainer.Q<TextField>("textField");
|
||
|
m_TextField.style.display = DisplayStyle.None;
|
||
|
|
||
|
// Update the Pill text if shader input name is changed
|
||
|
// we handle this in controller if we change it through SGBlackboardField, but its possible to change through PropertyNodeView as well
|
||
|
shaderInput.displayNameUpdateTrigger += newDisplayName => text = newDisplayName;
|
||
|
|
||
|
// Handles the upgrade fix for the old color property deprecation
|
||
|
if (shaderInput is AbstractShaderProperty property)
|
||
|
{
|
||
|
property.onAfterVersionChange += () =>
|
||
|
{
|
||
|
this.typeText = property.GetPropertyTypeString();
|
||
|
this.m_InspectorUpdateDelegate();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
Add(mainContainer);
|
||
|
|
||
|
RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
|
||
|
|
||
|
capabilities |= Capabilities.Selectable | Capabilities.Droppable | Capabilities.Deletable | Capabilities.Renamable;
|
||
|
|
||
|
ClearClassList();
|
||
|
AddToClassList("blackboardField");
|
||
|
|
||
|
this.name = "SGBlackboardField";
|
||
|
UpdateFromViewModel();
|
||
|
|
||
|
// add the right click context menu
|
||
|
IManipulator contextMenuManipulator = new ContextualMenuManipulator(AddContextMenuOptions);
|
||
|
this.AddManipulator(contextMenuManipulator);
|
||
|
this.AddManipulator(new SelectionDropper());
|
||
|
this.AddManipulator(new ContextualMenuManipulator(BuildFieldContextualMenu));
|
||
|
|
||
|
// When a display name is changed through the BlackboardPill, bind this callback to handle it with appropriate change action
|
||
|
var textInputElement = m_TextField.Q(TextField.textInputUssName);
|
||
|
textInputElement.RegisterCallback<FocusOutEvent>(e => { OnEditTextFinished(); });
|
||
|
|
||
|
ShaderGraphPreferences.onAllowDeprecatedChanged += UpdateTypeText;
|
||
|
|
||
|
RegisterCallback<MouseEnterEvent>(evt => OnMouseHover(evt, ViewModel.model));
|
||
|
RegisterCallback<MouseLeaveEvent>(evt => OnMouseHover(evt, ViewModel.model));
|
||
|
RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
|
||
|
|
||
|
var blackboard = ViewModel.parentView.GetFirstAncestorOfType<SGBlackboard>();
|
||
|
if (blackboard != null)
|
||
|
{
|
||
|
// These callbacks are used for the property dragging scroll behavior
|
||
|
RegisterCallback<DragEnterEvent>(blackboard.OnDragEnterEvent);
|
||
|
RegisterCallback<DragExitedEvent>(blackboard.OnDragExitedEvent);
|
||
|
|
||
|
// These callbacks are used for the property dragging scroll behavior
|
||
|
RegisterCallback<DragEnterEvent>(blackboard.OnDragEnterEvent);
|
||
|
RegisterCallback<DragExitedEvent>(blackboard.OnDragExitedEvent);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
~SGBlackboardField()
|
||
|
{
|
||
|
ShaderGraphPreferences.onAllowDeprecatedChanged -= UpdateTypeText;
|
||
|
}
|
||
|
|
||
|
void AddContextMenuOptions(ContextualMenuPopulateEvent evt)
|
||
|
{
|
||
|
// Checks if the reference name has been overridden and appends menu action to reset it, if so
|
||
|
if (shaderInput.isRenamable &&
|
||
|
!string.IsNullOrEmpty(shaderInput.overrideReferenceName))
|
||
|
{
|
||
|
evt.menu.AppendAction(
|
||
|
"Reset Reference",
|
||
|
e =>
|
||
|
{
|
||
|
var resetReferenceNameAction = new ResetReferenceNameAction();
|
||
|
resetReferenceNameAction.shaderInputReference = shaderInput;
|
||
|
ViewModel.requestModelChangeAction(resetReferenceNameAction);
|
||
|
m_ResetReferenceNameTrigger();
|
||
|
},
|
||
|
DropdownMenuAction.AlwaysEnabled);
|
||
|
}
|
||
|
|
||
|
if (shaderInput is ColorShaderProperty colorProp)
|
||
|
{
|
||
|
PropertyNodeView.AddMainColorMenuOptions(evt, colorProp, controller.graphData, m_InspectorUpdateDelegate);
|
||
|
}
|
||
|
|
||
|
|
||
|
if (shaderInput is Texture2DShaderProperty texProp)
|
||
|
{
|
||
|
PropertyNodeView.AddMainTextureMenuOptions(evt, texProp, controller.graphData, m_InspectorUpdateDelegate);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal void UpdateFromViewModel()
|
||
|
{
|
||
|
this.text = ViewModel.inputName;
|
||
|
this.icon = ViewModel.isInputExposed ? k_ExposedIcon : null;
|
||
|
this.typeText = ViewModel.inputTypeName;
|
||
|
}
|
||
|
|
||
|
ShaderInputViewController m_Controller;
|
||
|
|
||
|
// --- Begin ISGControlledElement implementation
|
||
|
public void OnControllerChanged(ref SGControllerChangedEvent e)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
public void OnControllerEvent(SGControllerEvent e)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
public ShaderInputViewController controller
|
||
|
{
|
||
|
get => m_Controller;
|
||
|
set
|
||
|
{
|
||
|
if (m_Controller != value)
|
||
|
{
|
||
|
if (m_Controller != null)
|
||
|
{
|
||
|
m_Controller.UnregisterHandler(this);
|
||
|
}
|
||
|
|
||
|
m_Controller = value;
|
||
|
|
||
|
if (m_Controller != null)
|
||
|
{
|
||
|
m_Controller.RegisterHandler(this);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SGController ISGControlledElement.controller => m_Controller;
|
||
|
|
||
|
// --- ISGControlledElement implementation
|
||
|
|
||
|
[Inspectable("Shader Input", null)]
|
||
|
public ShaderInput shaderInput => ViewModel.model;
|
||
|
|
||
|
public string inspectorTitle => ViewModel.inputName + " " + ViewModel.inputTypeName;
|
||
|
|
||
|
public object GetObjectToInspect()
|
||
|
{
|
||
|
return shaderInput;
|
||
|
}
|
||
|
|
||
|
Action m_InspectorUpdateDelegate;
|
||
|
|
||
|
public void SupplyDataToPropertyDrawer(IPropertyDrawer propertyDrawer, Action inspectorUpdateDelegate)
|
||
|
{
|
||
|
if (propertyDrawer is ShaderInputPropertyDrawer shaderInputPropertyDrawer)
|
||
|
{
|
||
|
// We currently need to do a halfway measure between the old way of handling stuff for property drawers (how FieldView and NodeView handle it)
|
||
|
// and how we want to handle it with the new style of controllers and views. Ideally we'd just hand the property drawer a view model and thats it.
|
||
|
// We've maintained all the old callbacks as they are in the PropertyDrawer to reduce possible halo changes and support PropertyNodeView functionality
|
||
|
// Instead we supply different underlying methods for the callbacks in the new SGBlackboardField,
|
||
|
// that way both code paths should work until we can refactor PropertyNodeView
|
||
|
shaderInputPropertyDrawer.GetViewModel(
|
||
|
ViewModel,
|
||
|
controller.graphData,
|
||
|
((triggerInspectorUpdate, modificationScope) =>
|
||
|
{
|
||
|
controller.DirtyNodes(modificationScope);
|
||
|
if (triggerInspectorUpdate)
|
||
|
inspectorUpdateDelegate();
|
||
|
}));
|
||
|
|
||
|
m_ResetReferenceNameTrigger = shaderInputPropertyDrawer.ResetReferenceName;
|
||
|
m_InspectorUpdateDelegate = inspectorUpdateDelegate;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void OnMouseDownEvent(MouseDownEvent e)
|
||
|
{
|
||
|
if ((e.clickCount == 2) && e.button == (int)MouseButton.LeftMouse && IsRenamable())
|
||
|
{
|
||
|
OpenTextEditor();
|
||
|
e.PreventDefault();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
e.StopPropagation();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void OnDragUpdatedEvent(DragUpdatedEvent evt)
|
||
|
{
|
||
|
if (m_SelectedNodes.Any())
|
||
|
{
|
||
|
foreach (var node in m_SelectedNodes)
|
||
|
{
|
||
|
node.RemoveFromClassList("hovered");
|
||
|
}
|
||
|
m_SelectedNodes.Clear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO: Move to controller? Feels weird for this to be directly communicating with PropertyNodes etc.
|
||
|
// Better way would be to send event to controller that notified of hover enter/exit and have other controllers be sent those events in turn
|
||
|
void OnMouseHover(EventBase evt, ShaderInput input)
|
||
|
{
|
||
|
var graphView = ViewModel.parentView.GetFirstAncestorOfType<MaterialGraphView>();
|
||
|
if (evt.eventTypeId == MouseEnterEvent.TypeId())
|
||
|
{
|
||
|
foreach (var node in graphView.nodes.ToList())
|
||
|
{
|
||
|
if (input is AbstractShaderProperty property)
|
||
|
{
|
||
|
if (node.userData is PropertyNode propertyNode)
|
||
|
{
|
||
|
if (propertyNode.property == input)
|
||
|
{
|
||
|
m_SelectedNodes.Add(node);
|
||
|
node.AddToClassList("hovered");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (input is ShaderKeyword keyword)
|
||
|
{
|
||
|
if (node.userData is KeywordNode keywordNode)
|
||
|
{
|
||
|
if (keywordNode.keyword == input)
|
||
|
{
|
||
|
m_SelectedNodes.Add(node);
|
||
|
node.AddToClassList("hovered");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (input is ShaderDropdown dropdown)
|
||
|
{
|
||
|
if (node.userData is DropdownNode dropdownNode)
|
||
|
{
|
||
|
if (dropdownNode.dropdown == input)
|
||
|
{
|
||
|
m_SelectedNodes.Add(node);
|
||
|
node.AddToClassList("hovered");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else if (evt.eventTypeId == MouseLeaveEvent.TypeId() && m_SelectedNodes.Any())
|
||
|
{
|
||
|
foreach (var node in m_SelectedNodes)
|
||
|
{
|
||
|
node.RemoveFromClassList("hovered");
|
||
|
}
|
||
|
m_SelectedNodes.Clear();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void UpdateTypeText()
|
||
|
{
|
||
|
if (shaderInput is AbstractShaderProperty asp)
|
||
|
{
|
||
|
typeText = asp.GetPropertyTypeString();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
internal void OpenTextEditor()
|
||
|
{
|
||
|
m_TextField.SetValueWithoutNotify(text);
|
||
|
m_TextField.style.display = DisplayStyle.Flex;
|
||
|
m_ContentItem.visible = false;
|
||
|
m_TextField.Q(TextField.textInputUssName).Focus();
|
||
|
m_TextField.SelectAll();
|
||
|
}
|
||
|
|
||
|
void OnEditTextFinished()
|
||
|
{
|
||
|
m_ContentItem.visible = true;
|
||
|
m_TextField.style.display = DisplayStyle.None;
|
||
|
|
||
|
if (text != m_TextField.text && String.IsNullOrWhiteSpace(m_TextField.text) == false && String.IsNullOrEmpty(m_TextField.text) == false)
|
||
|
{
|
||
|
var changeDisplayNameAction = new ChangeDisplayNameAction();
|
||
|
changeDisplayNameAction.shaderInputReference = shaderInput;
|
||
|
changeDisplayNameAction.newDisplayNameValue = m_TextField.text;
|
||
|
ViewModel.requestModelChangeAction(changeDisplayNameAction);
|
||
|
m_InspectorUpdateDelegate?.Invoke();
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Reset text field to original name
|
||
|
m_TextField.value = text;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected virtual void BuildFieldContextualMenu(ContextualMenuPopulateEvent evt)
|
||
|
{
|
||
|
evt.menu.AppendAction("Rename", (a) => OpenTextEditor(), DropdownMenuAction.AlwaysEnabled);
|
||
|
}
|
||
|
}
|
||
|
}
|