Editor/ShaderGraphPlus/Blackboard/BlackboardView/BlackboardGroupTreeNode.cs

Tree node class for a Shader Graph blackboard category/group in the editor. It represents a group of parameters, builds child parameter nodes, draws the group UI (icon, name, drag-drop visuals), handles renaming, reordering and moving parameters into/out of the group.

Reflection
using Editor;
using System.Text;
using static Editor.BaseItemWidget;

namespace ShaderGraphPlus;

public class BlackboardGroupTreeNode : BlackboardTreeNode
{
	private IEnumerable<BlackboardParameter> _referencedParameters;

	protected override bool CanHaveChildren => true;

	public override string Name
	{
		get => Value.Name;
		set => Value.Name = value;
	}

	public new CategoryData Value
	{
		get => base.Value as CategoryData;
		set => base.Value = value;
	}

	public new ShaderGraphPlusBlackboardTree TreeView
	{
		get => base.TreeView as ShaderGraphPlusBlackboardTree;
	}

	public override bool HasChildren => Value.ParameterReferences.Any();

	public override int ValueHash
	{
		get
		{
			if ( Value?.ParameterReferences is null )
				return 0;

			HashCode hc = new HashCode();

			foreach ( var reference in Value.ParameterReferences )
			{
				hc.Add( reference );
			}

			return hc.ToHashCode();
		}
	}

	public BlackboardGroupTreeNode( CategoryData categoryData ) : base( categoryData )
	{
	}

	public override string GetTooltip()
	{
		var baseTooltip = base.GetTooltip();

		var sb = new StringBuilder();

		sb.AppendLine( baseTooltip );

		sb.AppendLine( $"<hr />" );
		sb.AppendLine( $"<b>Parameters:</b>" );

		if ( _referencedParameters != null )
		{
			foreach ( var p in _referencedParameters )
			{
				sb.AppendLine( $"<br />" );
				sb.AppendLine( $"- {p.Name}" );
			}
		}

		return sb.ToString();
	}

	protected override void BuildChildren()
	{
		if ( HasChildren )
		{
			_referencedParameters = Value.GetReferencedParameters();
			SetChildren( _referencedParameters, x => new ShaderGraphPlusParameterTreeNode( x ) );
		}
		else
		{
			_referencedParameters = null;
			ClearChildren();
		}
	}

	protected override bool HasDescendant( object obj )
	{
		return Value.IsDescendant( obj as IGroupableBlackboardParameter );
	}

	public override void OnRename( VirtualWidget item, string text, List<TreeNode> selection = null )
	{
		if ( Name == text )
			return;

		using var undoScope = TreeView?.UndoScope( "Rename Group" );

		var categories = selection.Select( x => x.Value ).OfType<CategoryData>().ToArray();

		foreach ( var category in categories )
		{
			var id = 0;
			var newName = text;

			if ( TreeView.Graph.HasCategoryDataWithName( text ) )
			{
				while ( TreeView.Graph.HasCategoryDataWithName( $"{text}{id}" ) )
				{
					id++;
				}

				newName = $"{text}{id}";
			}

			category.Name = newName;
		}
	}

	protected override void OnPaintItemDropping( VirtualWidget item, Rect dropRect )
	{
		var dragEvent = TreeView.CurrentItemDragEvent;
		var sourceNode = dragEvent.Data.Object as BlackboardTreeNode;
		var targetNode = this;

		Paint.ClearPen();
		Paint.SetBrush( Theme.Blue );

		if ( dragEvent.DropEdge.HasFlag( ItemEdge.Top ) )
		{
			Paint.SetPen( Theme.Primary, 2f, PenStyle.Dot );
			Paint.DrawLine( dropRect.TopLeft, dropRect.TopRight );
		}
		else if ( dragEvent.DropEdge.HasFlag( ItemEdge.Bottom ) )
		{
			Paint.SetPen( Theme.Primary, 2f, PenStyle.Dot );
			Paint.DrawLine( dropRect.BottomLeft, dropRect.BottomRight );
		}
		else if ( sourceNode is not BlackboardGroupTreeNode && targetNode is BlackboardGroupTreeNode )
		{
			Paint.SetBrushAndPen( Theme.Blue.WithAlpha( 0.2f ), Theme.Blue );
			Paint.PenSize = 2;
			Paint.DrawRect( item.Rect, 4 );
		}
		else if ( sourceNode is BlackboardGroupTreeNode && targetNode is BlackboardGroupTreeNode )
		{
			var groupRect = item.Rect;
			groupRect.Left = 0;
			groupRect.Right = TreeView.Width;

			Paint.SetBrushAndPen( Theme.Blue.WithAlpha( 0.2f ), Theme.Blue );
			Paint.PenSize = 2;
			Paint.DrawRect( groupRect, 4 );
		}
	}

	protected override void PaintSpecial( Rect item )
	{
		float opacity = 0.9f;

		var r = item;
		r.Left += 4;

		Paint.SetDefaultFont();

		Paint.Pen = Color.White;
		Paint.DrawIcon( r, "folder", 12, TextFlag.LeftCenter | TextFlag.SingleLine );

		r.Left += 22;

		Paint.Pen = Theme.TextControl.WithAlphaMultiplied( opacity );

		r.Left += Paint.DrawText( r, Name, TextFlag.LeftCenter | TextFlag.SingleLine ).Width + 4;
	}

	protected override void InsertNodeAtEdge( BlackboardTreeNode source, BaseItemWidget.ItemEdge edge )
	{
		var graph = TreeView.Graph;

		if ( source.Value is IGroupableBlackboardParameter parameter && edge == ItemEdge.None )
		{
			using var undoScope = TreeView?.UndoScope( "Move Parameter To Group" );

			if ( parameter.IsGrouped )
			{
				RemoveFromGroup( parameter );
			}

			parameter.GroupReference = Value.Identifier;
			Value.ParameterReferences.Add( parameter.Identifier );

			TreeView.TreeNodes.Remove( source );
		}
		else if ( source.Value is CategoryData && edge != ItemEdge.Bottom )
		{
			var sourceCategory = source.Value as CategoryData;
			var sourcePriority = sourceCategory.Priority;
			var targetPriority = Value.Priority;

			if ( edge == ItemEdge.None && sourcePriority > targetPriority )
			{
				targetPriority++;
			}

			if ( targetPriority > TreeView.TreeNodes.Count() - 1 )
			{
				targetPriority = TreeView.TreeNodes.Count() - 1;
			}

			if ( sourcePriority != targetPriority )
			{
				using var undoScope = TreeView?.UndoScope( "Change Group Order" );

				TreeView?.Graph?.UpdateCategoryPriority( sourceCategory, targetPriority );
			}
		}
	}

	private int RemoveFromGroup( IGroupableBlackboardParameter sourceParameter )
	{
		if ( TreeView.Graph is ShaderGraphPlus graph )
		{
			if ( graph.TryFindCategoryData( sourceParameter.GroupReference, out var referencedGroup ) )
			{
				sourceParameter.GroupReference = Guid.Empty;

				referencedGroup.ParameterReferences.Remove( sourceParameter.Identifier );

				graph.UpdateParameter( sourceParameter );
				graph.UpdateCategoryData( referencedGroup );

				return referencedGroup.Priority;
			}
		}

		return -1;
	}
}

class BlackboardGroupSearchTreeNode : BlackboardGroupTreeNode
{
	public override bool HasChildren => false;

	public BlackboardGroupSearchTreeNode( CategoryData categoryData ) : base( categoryData )
	{
	}
}