Editor/Internal/NodeEditorPlus/Blackboard/BlackboardView/BlackboardTreeNode.cs

An abstract editor UI TreeNode for a node-editor blackboard. Draws row backgrounds, selection/hover visuals, handles drag/drop and context menu, and provides typed generic wrapper for node value and tree view.

File Access
using Editor;
using static Editor.BaseItemWidget;

namespace NodeEditorPlus.Blackboard;

public abstract class BlackboardTreeNode : TreeNode
{
	public override bool HasChildren => false;

	protected virtual bool CanHaveChildren => false;

	public virtual bool IsGrouped => false;

	public override bool CanEdit => true;

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

	public BlackboardTreeNode( object value ) : base( value )
	{
		Height = Theme.RowHeight;
	}

	public BlackboardTreeNode() : this( null )
	{
	}

	public override void OnPaint( VirtualWidget item )
	{
		var targetValue = Value;
		var isEven = item.Row % 2 == 0;
		var isHovered = item.Hovered;
		var selected = item.Selected || item.Pressed || item.Dragging;

		var fullSpanRect = item.Rect;
		fullSpanRect.Left = 0;
		fullSpanRect.Right = TreeView.Width;

		float opacity = 0.9f;

		if ( item.Dropping )
		{
			var dropRect = item.Rect;
			dropRect.Left = 0;
			dropRect.Right = TreeView.Width;

			OnPaintItemDropping( item, dropRect );
		}

		if ( selected )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.SelectedBackground.WithAlpha( opacity ) );
			Paint.DrawRect( fullSpanRect );
		}
		else if ( isHovered )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.SelectedBackground.WithAlpha( 0.25f ) );
			Paint.DrawRect( fullSpanRect );
		}
		if ( isEven )
		{
			Paint.ClearPen();
			Paint.SetBrush( Theme.SurfaceLightBackground.WithAlpha( 0.1f ) );
			Paint.DrawRect( fullSpanRect );
		}

		PaintSpecial( item.Rect );
	}

	protected virtual void OnPaintItemDropping( VirtualWidget item, Rect dropRect )
	{
		var dragEvent = TreeView.CurrentItemDragEvent;

		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 ) || dragEvent.DropEdge.HasFlag( ItemEdge.None ) )
		{
			Paint.SetPen( Theme.Primary, 2f, PenStyle.Dot );
			Paint.DrawLine( dropRect.BottomLeft, dropRect.BottomRight );
		}
	}

	protected abstract void PaintSpecial( Rect item );

	public override bool OnDragStart()
	{
		var drag = new Drag( TreeView );
		drag.Data.Object = this;
		drag.Execute();

		return true;
	}

	public override DropAction OnDragDrop( BaseItemWidget.ItemDragEvent e )
	{
		var targetNode = this;

		if ( e.Data.Object is BlackboardTreeNode sourceNode )
		{
			if ( sourceNode == targetNode || sourceNode.Value == Value )
				return DropAction.Ignore;

			if ( sourceNode.CanHaveChildren && targetNode.IsGrouped )
				return DropAction.Ignore;

			if ( sourceNode.IsDescendantOf( targetNode ) )
				return DropAction.Ignore;

			//Log.Info( $"Source '{sourceNode.Name}' Target '{Name}' DropEdge '{e.DropEdge}" );

			if ( e.IsDrop )
			{
				InsertNodeAtEdge( sourceNode, e.DropEdge );
			}

			return DropAction.Move;
		}

		return DropAction.Ignore;
	}

	protected abstract void InsertNodeAtEdge( BlackboardTreeNode source, ItemEdge edge );

	public override bool OnContextMenu()
	{
		var contextMenu = new ContextMenu( TreeView ) { Searchable = false };

		contextMenu.AddOption( "Rename", "label", TreeView.BeginRename, "editor.rename" );
		contextMenu.AddOption( "Delete", "delete", TreeView.DeleteSelection, "editor.delete" );

		contextMenu.AddSeparator();

		OnPopulateContextMenuSpecialOptions( contextMenu );

		contextMenu.OpenAtCursor( false );

		return true;
	}

	protected virtual void OnPopulateContextMenuSpecialOptions( ContextMenu m )
	{
	}

	protected bool TryGetValueAs<T>( out T value )
	{
		value = default;

		if ( Value.GetType() == typeof( T ) )
		{
			value = (T)Value;
			return true;
		}

		return false;
	}

	/// <summary>
	/// If the provided TreeNode is an ancestor of this TreeNode instance.
	/// </summary>
	/// <param name="potentialAncestor"></param>
	/// <returns></returns>
	protected bool IsDescendantOf( BlackboardTreeNode potentialAncestor )
	{
		var current = Parent as BlackboardTreeNode;
		while ( current != null )
		{
			if ( current == potentialAncestor )
				return true;
			current = current.Parent as BlackboardTreeNode;
		}
		return false;
	}
}

/// <summary>
/// A small wrapper that changes Value into the T type and TreeView into the Y Type.
/// </summary>
public abstract class BlackboardTreeNode<T, Y> : BlackboardTreeNode where T : class where Y : BlackboardTree
{
	public new T Value
	{
		get => base.Value as T;
		set => base.Value = value;
	}

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

	public BlackboardTreeNode( T value ) : base( value )
	{
	}

	public BlackboardTreeNode()
	{
	}
}