Files
sbox-public/game/editor/ActionGraph/Code/ActionControlWidget.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

534 lines
12 KiB
C#

using Facepunch.ActionGraphs;
using Sandbox;
using Sandbox.ActionGraphs;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
namespace Editor.ActionGraphs;
[CustomEditor( typeof( Delegate ) )]
public sealed class DelegateControlWidget : ControlWidget
{
internal sealed class DelegateWrapper : IList<IActionGraphDelegate>, IList
{
private readonly List<IActionGraphDelegate> _inner = new();
public SerializedProperty ParentProperty { get; }
public DelegateWrapper( SerializedProperty property )
{
ParentProperty = property;
ReadFromProperty();
}
public void ReadFromProperty()
{
_inner.Clear();
if ( ParentProperty.GetValue<Delegate>() is not { } deleg ) return;
foreach ( var inst in deleg.GetActionGraphInstances() )
{
_inner.Add( inst );
}
}
private void WriteToProperty()
{
if ( _inner.Count == 0 )
{
ParentProperty.SetValue<Delegate>( null );
return;
}
var delegates = _inner.Select( x => x.Delegate ).ToArray();
ParentProperty.SetValue( Delegate.Combine( delegates ) );
}
public IEnumerator<IActionGraphDelegate> GetEnumerator()
{
return _inner.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void Add( IActionGraphDelegate item )
{
Insert( Count, item );
}
int IList.Add( object value )
{
Add( value as IActionGraphDelegate );
return Count - 1;
}
public void Clear()
{
_inner.Clear();
WriteToProperty();
}
bool IList.Contains( object value )
{
return ((IList)_inner).Contains( value );
}
int IList.IndexOf( object value )
{
return ((IList)_inner).IndexOf( value );
}
void IList.Insert( int index, object value )
{
Insert( index, value as IActionGraphDelegate );
}
void IList.Remove( object value )
{
Remove( value as IActionGraphDelegate );
}
public bool Contains( IActionGraphDelegate item )
{
return _inner.Contains( item );
}
public void CopyTo( IActionGraphDelegate[] array, int arrayIndex )
{
_inner.CopyTo( array, arrayIndex );
}
public bool Remove( IActionGraphDelegate item )
{
if ( !_inner.Remove( item ) ) return false;
WriteToProperty();
return true;
}
void ICollection.CopyTo( Array array, int index )
{
((ICollection)_inner).CopyTo( array, index );
}
public int Count => _inner.Count;
bool ICollection.IsSynchronized => ((ICollection)_inner).IsSynchronized;
object ICollection.SyncRoot => ((ICollection)_inner).SyncRoot;
public bool IsReadOnly => !ParentProperty.IsEditable;
object IList.this[int index]
{
get => ((IList)_inner)[index];
set
{
this[index] = (IActionGraphDelegate)value;
}
}
public int IndexOf( IActionGraphDelegate item )
{
return _inner.IndexOf( item );
}
public void Insert( int index, IActionGraphDelegate item )
{
var delegType = ParentProperty.PropertyType;
var graph = ActionControlWidget.PrepareGraphForEditing( item?.Graph, ParentProperty, delegType );
item ??= graph.CreateDelegate( delegType );
_inner.Insert( index, item );
WriteToProperty();
}
public void RemoveAt( int index )
{
_inner.RemoveAt( index );
WriteToProperty();
}
bool IList.IsFixedSize => false;
public IActionGraphDelegate this[int index]
{
get => _inner[index];
set
{
_inner[index] = value;
WriteToProperty();
}
}
}
private readonly DelegateWrapper _delegateWrapper;
public DelegateControlWidget( SerializedProperty property )
: base( property )
{
var delegateType = property.PropertyType;
PaintBackground = false;
Layout = Layout.Column();
if ( IsActionDelegate( delegateType ) && !property.HasAttribute<SingleActionAttribute>() )
{
// Action delegates can have multiple subscribers, so show a list control
_delegateWrapper = new DelegateWrapper( property );
var sc = (SerializedCollection)EditorTypeLibrary.GetSerializedObject( _delegateWrapper );
sc.ParentProperty = property;
Layout.Add( new ListControlWidget( property, sc ) );
}
else
{
// Expression delegates (returning a value) can only have one subscriber, so show a single ActionControlWidget
Layout.Add( new ActionControlWidget( property ) );
}
}
/// <summary>
/// Does this delegate return <see cref="Void"/> or <see cref="Task"/>?
/// </summary>
private static bool IsActionDelegate( Type delegateType )
{
return NodeBinding.FromDelegateType( delegateType, EditorNodeLibrary ).Kind == NodeKind.Action;
}
public override void FromClipboardString( string clipboard )
{
base.FromClipboardString( clipboard );
if ( _delegateWrapper is { } wrapper )
{
wrapper.ReadFromProperty();
}
}
}
[CustomEditor( typeof( ActionGraph ) ), CustomEditor( typeof( IActionGraphDelegate ) )]
public sealed class ActionControlWidget : ControlWidget
{
private ActionGraphView _openView;
public ActionControlWidget( SerializedProperty property ) : base( property )
{
Cursor = CursorShape.Finger;
AcceptDrops = true;
}
private ActionGraph Graph
{
get
{
try
{
if ( SerializedProperty.PropertyType == typeof( ActionGraph ) )
{
return SerializedProperty.GetValue<ActionGraph>();
}
if ( SerializedProperty.PropertyType.IsAssignableTo( typeof( IActionGraphDelegate ) ) )
{
return SerializedProperty.GetValue<IActionGraphDelegate>()?.Graph;
}
return SerializedProperty.GetValue<Delegate>()?.GetActionGraphInstance()?.Graph;
}
catch ( InvalidCastException )
{
return null;
}
}
}
private Type DelegateType
{
get
{
var property = SerializedProperty;
if ( property.Parent is SerializedCollection { TargetObject: DelegateControlWidget.DelegateWrapper wrapper } )
{
property = wrapper.ParentProperty;
}
return property.PropertyType.IsAssignableTo( typeof( Delegate ) )
? property.PropertyType
: null;
}
}
private static object FindHostObject( SerializedProperty property )
{
var parent = property.Parent;
while ( parent != null )
{
if ( parent.IsMultipleTargets )
{
return null;
}
switch ( parent.Targets.FirstOrDefault() )
{
case GameObject go:
return go;
case Component component:
return component.GameObject;
case GameResource resource:
return resource;
}
parent = parent.ParentProperty?.Parent;
}
return null;
}
private void UpdateProperty( ActionGraph graph )
{
if ( SerializedProperty.PropertyType == typeof( ActionGraph ) )
{
SerializedProperty.SetValue( graph );
}
else if ( SerializedProperty.PropertyType.IsAssignableTo( typeof( IActionGraphDelegate ) ) )
{
var inst = SerializedProperty.GetValue<IActionGraphDelegate>();
SerializedProperty.SetValue( graph.CreateDelegate( DelegateType, inst?.Defaults ) );
}
else
{
var inst = SerializedProperty.GetValue<Delegate>()?.GetActionGraphInstance();
SerializedProperty.SetValue( graph.CreateDelegate( DelegateType, inst?.Defaults ).Delegate );
}
SerializedProperty.Parent.NoteChanged( SerializedProperty );
if ( IsValid )
{
SignalValuesChanged();
}
}
private ActionGraph PrepareGraphForEditing()
{
var oldGraph = Graph;
var newGraph = PrepareGraphForEditing( oldGraph, SerializedProperty, DelegateType );
if ( oldGraph != newGraph )
{
UpdateProperty( newGraph );
}
return newGraph;
}
internal static ActionGraph PrepareGraphForEditing( ActionGraph graph, SerializedProperty property, Type delegateType )
{
graph ??= delegateType is not null
? ActionGraph.CreateDelegate( EditorNodeLibrary, delegateType ).Graph
: ActionGraph.CreateEmpty( EditorNodeLibrary );
if ( string.IsNullOrEmpty( graph.Title ) )
{
graph.Title = property.DisplayName;
}
if ( FindHostObject( property ) is { } obj )
{
var eventArgs = new FindGraphTargetEvent( graph ) { TargetType = obj.GetType(), TargetValue = obj };
EditorEvent.Run( FindGraphTargetEvent.EventName, eventArgs );
if ( eventArgs.TargetType is { } type && eventArgs.TargetValue is null )
{
EditorActionGraph.SetTargetType( graph, type );
}
else if ( eventArgs.TargetValue is { } value )
{
EditorActionGraph.SetTarget( graph, value );
}
graph.SourceLocation ??= obj switch
{
GameResource resource => new GameResourceSourceLocation( resource ),
GameObject go => go.Scene.GetSourceLocation(),
_ => null
};
}
graph.AddRequiredNodes();
return graph;
}
private void OpenEditor()
{
var graph = PrepareGraphForEditing();
var view = ActionGraphView.Open( graph );
if ( _openView == view )
{
return;
}
_openView = view;
view.Saved += () => UpdateProperty( view.ActionGraph );
}
void Clear()
{
SerializedProperty.Parent.NoteStartEdit( SerializedProperty );
SerializedProperty.SetValue<object>( null );
SerializedProperty.Parent.NoteFinishEdit( SerializedProperty );
SignalValuesChanged();
}
protected override void OnMousePress( MouseEvent e )
{
if ( e.RightMouseButton )
{
if ( SerializedProperty.PropertyType.IsAssignableTo( typeof( IActionGraphDelegate ) ) || SerializedProperty.PropertyType == typeof( ActionGraph ) )
{
return;
}
var menu = new ContextMenu( this );
menu.AddOption( "Clear", "clear", Clear );
menu.OpenAtCursor();
}
if ( e.LeftMouseButton )
{
OpenEditor();
}
}
public override void StartEditing()
{
OpenEditor();
}
protected override void PaintOver()
{
var graph = Graph;
var rect = LocalRect.Shrink( 8, 0 );
var alpha = Paint.HasMouseOver ? 0.7f : 0.5f;
var errorCount = graph?.GetMessages().Count( x => x.IsError ) ?? 0;
// icon
{
Paint.SetPen( (graph is null ? Theme.Border : errorCount > 0 ? Theme.Red : Theme.Green).WithAlphaMultiplied( alpha ) );
var r = Paint.DrawIcon( rect, graph?.Icon ?? "account_tree", 17, TextFlag.LeftCenter );
rect.Left += r.Width + 8;
}
if ( graph is null )
{
Paint.SetPen( Theme.Border.WithAlphaMultiplied( alpha ) );
Paint.DrawText( rect, "Empty Action", TextFlag.LeftCenter );
}
else
{
var title = graph.Title;
var description = errorCount > 0 ? $"{errorCount} Error{(errorCount == 1 ? "" : "s")}" : graph.Description;
if ( string.IsNullOrWhiteSpace( title ) ) title = "Action";
Paint.SetPen( (errorCount > 0 ? Theme.Red.Lighten( 0.5f ) : Color.White).WithAlphaMultiplied( alpha ) );
var r = Paint.DrawText( rect, title, TextFlag.LeftCenter );
rect.Left += r.Width + 4;
if ( !string.IsNullOrWhiteSpace( description ) )
{
Paint.SetDefaultFont( 7 );
Paint.SetPen( Theme.Text.WithAlphaMultiplied( alpha * 0.5f ) );
Paint.DrawText( rect, description, TextFlag.LeftCenter );
}
}
}
private static bool TryGetDraggedAction( DragEvent ev, out ActionGraphResource resource )
{
resource = null;
if ( ev.Data.Assets.FirstOrDefault() is not { IsInstalled: true } asset )
{
return false;
}
if ( !string.Equals( Path.GetExtension( asset.AssetPath ), ".action", StringComparison.OrdinalIgnoreCase ) )
{
return false;
}
return ResourceLibrary.TryGet( asset.AssetPath, out resource );
}
public override void OnDragHover( DragEvent ev )
{
ev.Action = TryGetDraggedAction( ev, out _ ) ? DropAction.Link : DropAction.Ignore;
}
public override void OnDragDrop( DragEvent ev )
{
if ( !TryGetDraggedAction( ev, out var resource ) )
{
return;
}
var graph = PrepareGraphForEditing();
var graphNodeType = new GraphNodeType( resource );
var node = graphNodeType.CreateNode( graph );
graph.InputNode?.UpdateParameters();
if ( graph.InputNode?.Outputs.Signal is { } signal && node.Inputs.TryGetValue( ParameterNames.Signal, out var inSignal ) )
{
inSignal.SetLink( signal );
}
if ( graph.TargetOutput is { } outTarget && node.Inputs.Values.FirstOrDefault( x => x.IsTarget ) is { } inTarget )
{
inTarget.SetLink( outTarget );
}
if ( graph.UserData["LastInsertedId"]?.Deserialize<int>() is { } lastInsertedId && graph.Nodes.TryGetValue( lastInsertedId, out var lastInserted ) )
{
var lastInsertedPos = lastInserted.UserData["Position"]?.Deserialize<Vector2>() ?? Vector2.Zero;
node.UserData["Position"] = Json.ToNode( new Vector2( lastInsertedPos.x, lastInsertedPos.y + 64f ) );
}
else
{
var maxPosX = graph.Nodes.Values.Max( x => x.UserData["Position"]?.Deserialize<Vector2>().x ?? 0f );
node.UserData["Position"] = Json.ToNode( new Vector2( maxPosX + 256f, 0f ) );
}
graph.UserData["LastInsertedId"] = node.Id;
ActionGraphView.Rebuild( graph );
}
}