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.
 
 
 
 
 

813 lines
31 KiB

using System;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEditor.ShaderGraph.Drawing.Views;
using UnityEditor.ShaderGraph.Internal;
using UnityEngine;
using UnityEngine.UIElements;
using System.Linq;
using ContextualMenuManipulator = UnityEngine.UIElements.ContextualMenuManipulator;
namespace UnityEditor.ShaderGraph.Drawing
{
sealed class SGBlackboardCategory : GraphElement, ISGControlledElement<BlackboardCategoryController>, ISelection, IComparable<SGBlackboardCategory>
{
// --- Begin ISGControlledElement implementation
public void OnControllerChanged(ref SGControllerChangedEvent e)
{
}
public void OnControllerEvent(SGControllerEvent e)
{
}
public BlackboardCategoryController 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
BlackboardCategoryController m_Controller;
BlackboardCategoryViewModel m_ViewModel;
public BlackboardCategoryViewModel viewModel => m_ViewModel;
const string k_StylePath = "Styles/SGBlackboard";
const string k_UxmlPath = "UXML/Blackboard/SGBlackboardCategory";
VisualElement m_DragIndicator;
VisualElement m_MainContainer;
VisualElement m_Header;
Label m_TitleLabel;
Foldout m_Foldout;
TextField m_TextField;
internal TextField textField => m_TextField;
VisualElement m_RowsContainer;
int m_InsertIndex;
SGBlackboard blackboard => m_ViewModel.parentView as SGBlackboard;
bool m_IsDragInProgress;
bool m_WasHoverExpanded;
bool m_DroppedOnBottomEdge;
bool m_DroppedOnTopEdge;
bool m_RenameInProgress;
ContextualMenuManipulator m_ContextMenuManipulator;
SelectionDropper m_SelectionDropperManipulator;
public delegate bool CanAcceptDropDelegate(ISelectable selected);
public CanAcceptDropDelegate canAcceptDrop { get; set; }
int InsertionIndex(Vector2 pos)
{
// For an empty category can always just insert at the start
if (this.childCount == 0)
return 0;
var blackboardRows = this.Query<SGBlackboardRow>().ToList();
for (int index = 0; index < blackboardRows.Count; index++)
{
var blackboardRow = blackboardRows[index];
var localPosition = this.ChangeCoordinatesTo(blackboardRow, pos);
if (blackboardRow.ContainsPoint(localPosition))
{
return index;
}
}
return -1;
}
static VisualElement FindCategoryDirectChild(SGBlackboardCategory blackboardCategory, VisualElement element)
{
VisualElement directChild = element;
while ((directChild != null) && (directChild != blackboardCategory))
{
if (directChild.parent == blackboardCategory)
{
return directChild;
}
directChild = directChild.parent;
}
return null;
}
internal SGBlackboardCategory(BlackboardCategoryViewModel categoryViewModel, BlackboardCategoryController inController)
{
m_ViewModel = categoryViewModel;
controller = inController;
userData = controller.Model;
// Setup VisualElement from Stylesheet and UXML file
var tpl = Resources.Load(k_UxmlPath) as VisualTreeAsset;
m_MainContainer = tpl.Instantiate();
m_MainContainer.AddToClassList("mainContainer");
m_Header = m_MainContainer.Q("categoryHeader");
m_TitleLabel = m_MainContainer.Q<Label>("categoryTitleLabel");
m_Foldout = m_MainContainer.Q<Foldout>("categoryTitleFoldout");
m_RowsContainer = m_MainContainer.Q("rowsContainer");
m_TextField = m_MainContainer.Q<TextField>("textField");
m_TextField.style.display = DisplayStyle.None;
hierarchy.Add(m_MainContainer);
m_DragIndicator = m_MainContainer.Q("dragIndicator");
m_DragIndicator.visible = false;
hierarchy.Add(m_DragIndicator);
// setting Capabilities.Selectable adds a ClickSelector
capabilities |= Capabilities.Selectable | Capabilities.Movable | Capabilities.Droppable | Capabilities.Deletable | Capabilities.Renamable | Capabilities.Copiable;
ClearClassList();
AddToClassList("blackboardCategory");
// add the right click context menu
m_ContextMenuManipulator = new ContextualMenuManipulator(AddContextMenuOptions);
this.AddManipulator(m_ContextMenuManipulator);
// add drag and drop manipulator
m_SelectionDropperManipulator = new SelectionDropper();
this.AddManipulator(m_SelectionDropperManipulator);
RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
var textInputElement = m_TextField.Q(TextField.textInputUssName);
textInputElement.RegisterCallback<FocusOutEvent>(e => { OnEditTextFinished(); }, TrickleDown.TrickleDown);
// Register hover callbacks
RegisterCallback<MouseEnterEvent>(OnHoverStartEvent);
RegisterCallback<MouseLeaveEvent>(OnHoverEndEvent);
// Register drag callbacks
RegisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
RegisterCallback<DragPerformEvent>(OnDragPerformEvent);
RegisterCallback<DragLeaveEvent>(OnDragLeaveEvent);
var styleSheet = Resources.Load<StyleSheet>(k_StylePath);
styleSheets.Add(styleSheet);
m_InsertIndex = -1;
// Update category title from view model
title = m_ViewModel.name;
this.viewDataKey = viewModel.associatedCategoryGuid;
if (String.IsNullOrEmpty(title))
{
m_Foldout.visible = false;
m_Foldout.RemoveFromHierarchy();
}
else
{
TryDoFoldout(m_ViewModel.isExpanded);
m_Foldout.RegisterCallback<ChangeEvent<bool>>(OnFoldoutToggle);
}
// Remove the header element if this is the default category
if (!controller.Model.IsNamedCategory())
headerVisible = false;
}
public override VisualElement contentContainer { get { return m_RowsContainer; } }
public override string title
{
get => m_TitleLabel.text;
set
{
m_TitleLabel.text = value;
if (m_TitleLabel.text == String.Empty)
{
AddToClassList("unnamed");
}
else
{
RemoveFromClassList("unnamed");
}
}
}
public bool headerVisible
{
get { return m_Header.parent != null; }
set
{
if (value == (m_Header.parent != null))
return;
if (value)
{
m_MainContainer.Add(m_Header);
}
else
{
m_MainContainer.Remove(m_Header);
}
}
}
void SetDragIndicatorVisible(bool visible)
{
if(m_DragIndicator != null)
m_DragIndicator.visible = visible;
}
public bool CategoryContains(List<ISelectable> selection)
{
if (selection == null)
return false;
// Look for at least one selected element in this category to accept drop
foreach (ISelectable selected in selection)
{
VisualElement selectedElement = selected as VisualElement;
if (selected != null && Contains(selectedElement))
{
if (canAcceptDrop == null || canAcceptDrop(selected))
return true;
}
}
return false;
}
void OnFoldoutToggle(ChangeEvent<bool> evt)
{
if (evt.previousValue != evt.newValue)
{
var isExpandedAction = new ChangeCategoryIsExpandedAction();
if (selection.Contains(this)) // expand all selected if the foldout is part of a selection
isExpandedAction.categoryGuids = selection.OfType<SGBlackboardCategory>().Select(s => s.viewModel.associatedCategoryGuid).ToList();
else
isExpandedAction.categoryGuids = new List<string>() { viewModel.associatedCategoryGuid };
isExpandedAction.isExpanded = evt.newValue;
isExpandedAction.editorPrefsBaseKey = blackboard.controller.editorPrefsBaseKey;
viewModel.requestModelChangeAction(isExpandedAction);
}
}
internal void TryDoFoldout(bool expand)
{
m_Foldout.SetValueWithoutNotify(expand);
if (!expand)
{
m_DragIndicator.visible = true;
m_RowsContainer.RemoveFromHierarchy();
}
else
{
m_DragIndicator.visible = false;
m_MainContainer.Add(m_RowsContainer);
}
var key = $"{blackboard.controller.editorPrefsBaseKey}.{viewDataKey}.{ChangeCategoryIsExpandedAction.kEditorPrefKey}";
EditorPrefs.SetBool(key, expand);
}
void OnMouseDownEvent(MouseDownEvent e)
{
// Handles double-click with left mouse, which should trigger a rename action on this category
if ((e.clickCount == 2) && e.button == (int)MouseButton.LeftMouse && IsRenamable())
{
OpenTextEditor();
e.StopPropagation();
// Prevent MouseDown from refocusing the Label on PostDispatch
focusController.IgnoreEvent(e);
}
else if (e.clickCount == 1 && e.button == (int)MouseButton.LeftMouse && IsRenamable())
{
// Select the child elements within this category (the field views)
var fieldViews = this.Query<SGBlackboardField>();
foreach (var child in fieldViews.ToList())
{
this.AddToSelection(child);
}
}
}
internal void OpenTextEditor()
{
m_TextField.SetValueWithoutNotify(title);
m_TextField.style.display = DisplayStyle.Flex;
m_TitleLabel.visible = false;
m_TextField.Q(TextField.textInputUssName).Focus();
m_TextField.SelectAll();
m_RenameInProgress = true;
}
internal void OnEditTextFinished()
{
m_TitleLabel.visible = true;
m_TextField.style.display = DisplayStyle.None;
if (title != m_TextField.text && String.IsNullOrWhiteSpace(m_TextField.text) == false)
{
var changeCategoryNameAction = new ChangeCategoryNameAction();
changeCategoryNameAction.newCategoryNameValue = m_TextField.text;
changeCategoryNameAction.categoryGuid = m_ViewModel.associatedCategoryGuid;
m_ViewModel.requestModelChangeAction(changeCategoryNameAction);
}
else
{
// Reset text field to original name
m_TextField.value = title;
}
m_RenameInProgress = false;
}
void OnHoverStartEvent(MouseEnterEvent evt)
{
AddToClassList("hovered");
if (selection.OfType<SGBlackboardField>().Any()
&& controller.Model.IsNamedCategory()
&& m_IsDragInProgress
&& !viewModel.isExpanded)
{
m_WasHoverExpanded = true;
TryDoFoldout(true);
}
}
void OnHoverEndEvent(MouseLeaveEvent evt)
{
if (m_WasHoverExpanded && m_IsDragInProgress)
{
m_WasHoverExpanded = false;
TryDoFoldout(false);
}
RemoveFromClassList("hovered");
}
void OnDragUpdatedEvent(DragUpdatedEvent evt)
{
var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
// Don't show drag indicator if selection has categories,
// We don't want category drag & drop to be ambiguous with shader input drag & drop
if (selection.OfType<SGBlackboardCategory>().Any())
{
SetDragIndicatorVisible(false);
return;
}
// If can't find at least one blackboard field in the selection, don't update drag indicator
if (selection.OfType<SGBlackboardField>().Any() == false)
{
SetDragIndicatorVisible(false);
return;
}
m_IsDragInProgress = true;
Vector2 localPosition = evt.localMousePosition;
m_InsertIndex = InsertionIndex(localPosition);
if (m_InsertIndex != -1)
{
float indicatorY = 0;
bool inMoveRange = false;
// When category is empty
if (this.childCount == 0)
{
// This moves the indicator to the bottom of the category in case of an empty category
indicatorY = this.layout.height * 0.9f;
m_DragIndicator.style.marginBottom = 8;
inMoveRange = true;
}
else
{
m_DragIndicator.style.marginBottom = 0;
var relativePosition = new Vector2();
var childHeight = 0.0f;
VisualElement childAtInsertIndex = this[m_InsertIndex];
childHeight = childAtInsertIndex.layout.height;
relativePosition = this.ChangeCoordinatesTo(childAtInsertIndex, localPosition);
if (relativePosition.y > 0 && relativePosition.y < childHeight * 0.25f)
{
// Top Edge
inMoveRange = true;
indicatorY = childAtInsertIndex.ChangeCoordinatesTo(this, new Vector2(0, 0)).y;
m_DragIndicator.style.rotate = new StyleRotate(Rotate.None());
m_DroppedOnBottomEdge = false;
m_DroppedOnTopEdge = true;
}
else if (relativePosition.y > 0.75f * childHeight && relativePosition.y < childHeight)
{
// Bottom Edge
inMoveRange = true;
indicatorY = childAtInsertIndex.ChangeCoordinatesTo(this, new Vector2(0, 0)).y + childAtInsertIndex.layout.height;
//m_DragIndicator.style.rotate = new StyleRotate(new Rotate(-180));
m_DroppedOnBottomEdge = true;
m_DroppedOnTopEdge = false;
}
}
if (inMoveRange)
{
SetDragIndicatorVisible(true);
m_DragIndicator.style.width = layout.width;
var newPosition = indicatorY;
m_DragIndicator.style.top = newPosition;
}
else
SetDragIndicatorVisible(true);
}
else
SetDragIndicatorVisible(false);
if (m_InsertIndex != -1)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
}
}
void OnDragPerformEvent(DragPerformEvent evt)
{
var selection = DragAndDrop.GetGenericData("DragSelection") as List<ISelectable>;
m_IsDragInProgress = false;
// Don't show drag indicator if selection has categories,
// We don't want category drag & drop to be ambiguous with shader input drag & drop
if (selection.OfType<SGBlackboardCategory>().Any())
{
SetDragIndicatorVisible(false);
return;
}
if (m_InsertIndex == -1)
{
SetDragIndicatorVisible(false);
return;
}
// Map of containing categories to the actual dragged elements within them
SortedDictionary<SGBlackboardCategory, List<VisualElement>> draggedElements = new SortedDictionary<SGBlackboardCategory, List<VisualElement>>();
foreach (ISelectable selectedElement in selection)
{
var draggedElement = selectedElement as VisualElement;
if (draggedElement == null)
continue;
if (this.Contains(draggedElement))
{
if (draggedElements.ContainsKey(this))
draggedElements[this].Add(FindCategoryDirectChild(this, draggedElement));
else
draggedElements.Add(this, new List<VisualElement> { FindCategoryDirectChild(this, draggedElement) });
}
else
{
var otherCategory = draggedElement.GetFirstAncestorOfType<SGBlackboardCategory>();
if (otherCategory != null)
{
if (draggedElements.ContainsKey(otherCategory))
draggedElements[otherCategory].Add(FindCategoryDirectChild(otherCategory, draggedElement));
else
draggedElements.Add(otherCategory, new List<VisualElement> { FindCategoryDirectChild(otherCategory, draggedElement) });
}
}
}
if (draggedElements.Count == 0)
{
SetDragIndicatorVisible(false);
return;
}
foreach (var categoryToChildrenTuple in draggedElements)
{
var containingCategory = categoryToChildrenTuple.Key;
var childList = categoryToChildrenTuple.Value;
// Sorts the dragged elements from their relative order in their parent
childList.Sort((item1, item2) => containingCategory.IndexOf(item1).CompareTo(containingCategory.IndexOf(item2)));
}
int insertIndex = Mathf.Clamp(m_InsertIndex, 0, m_InsertIndex);
bool adjustedInsertIndex = false;
VisualElement lastInsertedElement = null;
/* Handles moving elements within a category */
foreach (var categoryToChildrenTuple in draggedElements)
{
var childList = categoryToChildrenTuple.Value;
VisualElement firstChild = childList.First();
foreach (var draggedElement in childList)
{
var blackboardField = draggedElement.Q<SGBlackboardField>();
ShaderInput shaderInput = null;
if (blackboardField != null)
shaderInput = blackboardField.controller.Model;
// Skip if this field is not contained by this category as we handle that in the next loop below
if (shaderInput == null || !this.Contains(blackboardField))
continue;
VisualElement categoryDirectChild = draggedElement;
int indexOfDraggedElement = IndexOf(categoryDirectChild);
bool listEndInsertion = false;
// Only find index for the first item
if (draggedElement == firstChild)
{
adjustedInsertIndex = true;
// Handles case of inserting after last item in list
if (insertIndex == childCount - 1 && m_DroppedOnBottomEdge)
{
listEndInsertion = true;
}
// Handles case of inserting after any item except the last in list
else if (m_DroppedOnBottomEdge)
insertIndex++;
insertIndex = Mathf.Clamp(insertIndex, 0, childCount - 1);
if (insertIndex != indexOfDraggedElement)
{
// If ever placing it at end of list, make sure to place after last item
if (listEndInsertion)
{
categoryDirectChild.PlaceInFront(this[insertIndex]);
}
else
{
categoryDirectChild.PlaceBehind(this[insertIndex]);
}
}
lastInsertedElement = firstChild;
}
// Place every subsequent row after that use PlaceInFront(), this prevents weird re-ordering issues as long as we can get the first index right
else
{
var indexOfFirstChild = this.IndexOf(lastInsertedElement);
categoryDirectChild.PlaceInFront(this[indexOfFirstChild]);
lastInsertedElement = categoryDirectChild;
}
if (insertIndex > childCount - 1 || listEndInsertion)
insertIndex = -1;
var moveShaderInputAction = new MoveShaderInputAction();
moveShaderInputAction.associatedCategoryGuid = viewModel.associatedCategoryGuid;
moveShaderInputAction.shaderInputReference = shaderInput;
moveShaderInputAction.newIndexValue = insertIndex;
m_ViewModel.requestModelChangeAction(moveShaderInputAction);
// Make sure to remove the element from the selection so it doesn't get re-handled by the blackboard as well, leads to duplicates
selection.Remove(blackboardField);
if (insertIndex > indexOfDraggedElement)
continue;
// If adding to the end of the list, we no longer need to increment the index
if (insertIndex != -1)
insertIndex++;
}
}
/* Handles moving elements from one category to another (including between different graph windows) */
// Handles case of inserting after item in list
if (!adjustedInsertIndex)
{
if (m_DroppedOnBottomEdge)
{
insertIndex++;
}
// Only ever do this for the first item
else if (m_DroppedOnTopEdge && insertIndex == 0)
{
insertIndex = Mathf.Clamp(insertIndex - 1, 0, childCount - 1);
}
}
else if (lastInsertedElement != null)
{
insertIndex = this.IndexOf(lastInsertedElement) + 1;
}
foreach (var categoryToChildrenTuple in draggedElements)
{
var childList = categoryToChildrenTuple.Value;
foreach (var draggedElement in childList)
{
var blackboardField = draggedElement.Q<SGBlackboardField>();
ShaderInput shaderInput = null;
if (blackboardField != null)
shaderInput = blackboardField.controller.Model;
if (shaderInput == null)
continue;
// If the blackboard field is contained by this category its already been handled above, skip
if (this.Contains(blackboardField))
continue;
var addItemToCategoryAction = new AddItemToCategoryAction();
addItemToCategoryAction.categoryGuid = viewModel.associatedCategoryGuid;
addItemToCategoryAction.addActionSource = AddItemToCategoryAction.AddActionSource.DragDrop;
addItemToCategoryAction.itemToAdd = shaderInput;
// If adding to end of list, make the insert index -1 to ensure op goes through as expected
if (insertIndex > childCount - 1)
insertIndex = -1;
addItemToCategoryAction.indexToAddItemAt = insertIndex;
m_ViewModel.requestModelChangeAction(addItemToCategoryAction);
// Make sure to remove the element from the selection so it doesn't get re-handled by the blackboard as well, leads to duplicates
selection.Remove(blackboardField);
// If adding to the end of the list, we no longer need to increment the index
if (insertIndex != -1)
insertIndex++;
}
}
SetDragIndicatorVisible(false);
}
void OnDragLeaveEvent(DragLeaveEvent evt)
{
SetDragIndicatorVisible(false);
}
internal void OnDragActionCanceled()
{
SetDragIndicatorVisible(false);
m_IsDragInProgress = false;
}
public override void Select(VisualElement selectionContainer, bool additive)
{
if (controller == null)
return;
// Don't add the un-named/default category to graph selections
if (controller.Model.IsNamedCategory())
{
base.Select(selectionContainer, additive);
}
}
public override void OnSelected()
{
AddToClassList("selected");
}
public override void OnUnselected()
{
RemoveFromClassList("selected");
}
public void AddToSelection(ISelectable selectable)
{
// Don't add the un-named/default category to graph selections,
if (controller.Model.IsNamedCategory() == false && selectable == this)
return;
// Don't add to selection if a rename op is in progress
if (m_RenameInProgress)
{
RemoveFromSelection(this);
return;
}
var materialGraphView = m_ViewModel.parentView.GetFirstAncestorOfType<MaterialGraphView>();
materialGraphView?.AddToSelection(selectable);
}
public void RemoveFromSelection(ISelectable selectable)
{
var materialGraphView = m_ViewModel.parentView.GetFirstAncestorOfType<MaterialGraphView>();
// If we're de-selecting the category itself
if (selectable == this)
{
materialGraphView?.RemoveFromSelection(selectable);
// Also deselect the child elements within this category (the field views)
var fieldViews = this.Query<SGBlackboardField>();
foreach (var child in fieldViews.ToList())
{
materialGraphView?.RemoveFromSelection(child);
}
}
// If a category is unselected, only then can the children beneath it be deselected
else if (selection.Contains(this) == false)
{
materialGraphView?.RemoveFromSelection(selectable);
}
}
public void ClearSelection()
{
RemoveFromClassList("selected");
var materialGraphView = m_ViewModel.parentView.GetFirstAncestorOfType<MaterialGraphView>();
materialGraphView?.ClearSelection();
}
public List<ISelectable> selection
{
get
{
var selectionProvider = m_ViewModel.parentView.GetFirstAncestorOfType<ISelectionProvider>();
if (selectionProvider?.GetSelection != null)
return selectionProvider.GetSelection;
return new List<ISelectable>();
}
}
void RequestCategoryDelete()
{
var materialGraphView = blackboard.ParentView as MaterialGraphView;
materialGraphView?.deleteSelection?.Invoke("Delete", GraphView.AskUser.DontAskUser);
}
void AddContextMenuOptions(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("Delete", evt => RequestCategoryDelete());
evt.menu.AppendAction("Rename", (a) => OpenTextEditor(), DropdownMenuAction.AlwaysEnabled);
// Don't allow the default/un-named category to have right-click menu options
if (controller.Model.IsNamedCategory())
{
evt.menu.AppendAction("Delete", evt => RequestCategoryDelete());
}
}
public int CompareTo(SGBlackboardCategory other)
{
if (other == null)
return 1;
var thisBlackboard = this.blackboard;
var otherBlackboard = other.blackboard;
return thisBlackboard.IndexOf(this).CompareTo(otherBlackboard.IndexOf(other));
}
public void Dispose()
{
if (m_Controller == null)
return;
UnregisterCallback<MouseDownEvent>(OnMouseDownEvent);
var textInputElement = m_TextField.Q(TextField.textInputUssName);
textInputElement.UnregisterCallback<FocusOutEvent>(e => { OnEditTextFinished(); }, TrickleDown.TrickleDown);
UnregisterCallback<MouseEnterEvent>(OnHoverStartEvent);
UnregisterCallback<MouseLeaveEvent>(OnHoverEndEvent);
UnregisterCallback<DragUpdatedEvent>(OnDragUpdatedEvent);
UnregisterCallback<DragPerformEvent>(OnDragPerformEvent);
UnregisterCallback<DragLeaveEvent>(OnDragLeaveEvent);
m_Foldout.UnregisterCallback<ChangeEvent<bool>>(OnFoldoutToggle);
this.RemoveManipulator(m_ContextMenuManipulator);
this.RemoveManipulator(m_SelectionDropperManipulator);
m_Controller = null;
m_ViewModel = null;
m_MainContainer = null;
m_Header = null;
m_TitleLabel = null;
m_Foldout = null;
m_RowsContainer = null;
m_TextField = null;
m_DragIndicator = null;
m_ContextMenuManipulator = null;
m_SelectionDropperManipulator = null;
RemoveFromHierarchy();
}
}
}