Files
sbox-public/game/editor/ActionGraph/Code/Properties.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

697 lines
16 KiB
C#

using Editor.NodeEditor;
using Facepunch.ActionGraphs;
using Sandbox;
using Sandbox.Internal;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
namespace Editor.ActionGraphs;
public class Properties : Widget
{
private static HashSet<Type> DefaultNumericTypes { get; } = new()
{
typeof(int),
typeof(float),
typeof(Vector2),
typeof(Vector3),
typeof(Vector4)
};
private static HashSet<Type> DefaultTypes { get; } = new()
{
typeof(int),
typeof(float),
typeof(Vector2),
typeof(Vector3),
typeof(Vector4),
typeof(Color),
typeof(string),
typeof(bool),
typeof(object)
};
public static Type DefaultType { get; } = Either.CreateType( DefaultTypes );
public static Type DefaultNumericType { get; } = Either.CreateType( DefaultNumericTypes );
public static Type PredictBestType( Node.IParameter target )
{
if ( target is not Node.Input )
{
return DefaultType;
}
if ( !target.Node.Definition.IsOperator() )
{
return DefaultType;
}
var otherInput = target.Name switch
{
"a" => target.Node.Inputs.TryGetValue( "b", out var b ) ? b : null,
"b" => target.Node.Inputs.TryGetValue( "a", out var a ) ? a : null,
_ => null
};
switch ( target.Node.Definition.Identifier )
{
case "op.conditional":
case "op.coalesce":
case "op.equal":
case "op.notequal":
return otherInput?.SourceType ?? DefaultType;
default:
return otherInput?.SourceType is { } otherType ? typeof( Either<,> ).MakeGenericType( otherType, DefaultNumericType ) : DefaultNumericType;
}
}
private record PropertyRow( SerializedProperty Property, ControlWidget Widget, Node.IParameter Parameter = null, int? Index = null );
private SerializedProperty _lastActive;
private object _target;
private readonly List<PropertyRow> _rows = new();
Layout Content;
private ControlWidget _startEditingTarget;
public MainWindow MainWindow { get; }
public object Target
{
get => _target;
set
{
if ( _target == value )
{
return;
}
_target = value;
_contentInvalid = true;
}
}
private bool _contentInvalid;
public Properties( MainWindow mainWindow ) : base( null )
{
MainWindow = mainWindow;
Name = "Inspector";
WindowTitle = "Inspector";
SetWindowIcon( "manage_search" );
Layout = Layout.Column();
Content = Layout.AddColumn();
SetSizeMode( SizeMode.Default, SizeMode.CanShrink );
MainWindow.SelectionChanged += OnSelectionChanged;
MainWindow.FocusedOnInput += FocusOnInput;
}
public override void OnDestroyed()
{
base.OnDestroyed();
MainWindow.SelectionChanged -= OnSelectionChanged;
MainWindow.FocusedOnInput -= FocusOnInput;
}
private void OnSelectionChanged( object target )
{
Target = target;
}
[EditorEvent.Frame]
public void Frame()
{
var target = Target;
if ( target is NodeUI nodeUi )
{
target = nodeUi.Node;
}
if ( target is EditorNode node )
{
if ( !node.Node.IsValid )
{
Target = null;
}
else
{
node.MarkDirty();
}
}
if ( _contentInvalid )
{
_contentInvalid = false;
RebuildContent();
}
if ( _startEditingTarget.IsValid() )
{
_startEditingTarget.StartEditing();
_startEditingTarget = null;
}
SerializedProperty active = null;
foreach ( var row in _rows )
{
if ( row.Widget.IsControlActive )
{
active = row.Property;
break;
}
}
if ( _lastActive != active )
{
_lastActive = active;
if ( active != null )
{
MainWindow.FocusedView?.PushUndo( $"Set Property ({active?.Name})" );
}
}
}
protected override void OnFocus( FocusChangeReason reason )
{
MainWindow.FocusedView?.PushUndo( "Edit Properties" );
}
public void FocusOnInput( Node.Input input, int? index )
{
if ( _contentInvalid )
{
_contentInvalid = false;
RebuildContent();
}
var match = _rows.FirstOrDefault( x => x.Parameter == input && x.Index == index )
?? _rows.FirstOrDefault( x => x.Parameter == input && x.Index == null );
_startEditingTarget = match?.Widget;
}
void RebuildContent()
{
_lastActive = null;
Content.Clear( true );
_rows.Clear();
if ( _target == null )
{
return;
}
var ps = new ControlSheet();
var target = Target;
Content.Add( ps );
Content.AddStretchCell();
if ( target is NodeUI nodeUi )
{
target = nodeUi.Node;
}
else
{
nodeUi = null;
}
if ( target is CommentEditorNode commentNode )
{
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( commentNode ) );
obj.OnPropertyChanged += _ => commentNode.MarkDirty();
}
else if ( target is EditorActionGraph actionGraph )
{
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( actionGraph ) );
var inputNode = actionGraph.FindNode( actionGraph.Graph.InputNode );
obj.OnPropertyChanged += _ => inputNode?.MarkDirty();
}
else if ( target is EditorNode node )
{
var obj = AddObject( ps, EditorTypeLibrary.GetSerializedObject( node ) );
obj.OnPropertyChanged += _ => nodeUi!.Position = node.Position;
obj = AddObject( ps, new SerializedActionNode( this, node ) );
obj.OnPropertyChanged += _ => node.MarkDirty();
}
}
private SerializedObject AddObject( ControlSheet ps, SerializedObject obj )
{
ps.AddObject( obj );
return obj;
}
}
internal class SerializedActionNode : SerializedObject
{
private static bool CanEditInputType( Type type )
{
if ( type is null ) return false;
if ( Either.IsEitherType( type ) ) return true;
if ( type.IsAbstract || type.IsInterface || type.IsArray || type.ContainsGenericParameters )
{
return false;
}
if ( type == typeof( Task ) || type == typeof( Task[] ) ) return false;
if ( type == typeof( Variable ) ) return false;
// Should use scene ref / resource ref nodes instead
if ( type == typeof( GameObject ) ) return false;
if ( type.IsAssignableTo( typeof( Component ) ) ) return false;
if ( type.IsAssignableTo( typeof( Resource ) ) ) return false;
return true;
}
private static HashSet<string> HiddenProperties { get; } = new()
{
"graph/graph",
"input.value/_name",
"var/_var",
"property/_name", "property/_type",
"call/_name", "call/_type", "call/_isStatic",
"new/_type",
"switch/cases"
};
private static HashSet<string> HiddenInputs { get; } = new()
{
};
public SerializedActionNode( Properties properties, EditorNode node )
{
PropertyList = new();
foreach ( var (name, property) in node.Node.Properties )
{
if ( property.Display.Hidden is true )
{
continue;
}
if ( HiddenProperties.Contains( $"{node.Definition.Identifier}/{name}" ) )
{
continue;
}
PropertyList.Add( new SerializedNodeProperty( properties, node, property ) );
}
if ( node.Definition.Identifier == "switch" )
{
AddSwitchNodeProperties( properties, node );
}
foreach ( var (name, input) in node.Node.Inputs )
{
if ( HiddenInputs.Contains( $"{node.Definition.Identifier}/{name}" ) )
{
continue;
}
if ( !input.IsArray )
{
if ( input.IsLinked && input.Link?.TryGetConstant( out _ ) is not true )
{
continue;
}
if ( !CanEditInputType( input.Type ) )
{
continue;
}
PropertyList.Add( new SerializedNodeInput( properties, node, input ) );
}
else
{
if ( input.Link is not null )
{
continue;
}
if ( !CanEditInputType( input.ElementType ) )
{
continue;
}
var count = input.LinkArray?.Count ?? 0;
for ( var i = 0; i <= count; ++i )
{
if ( i < count && input.LinkArray?[i].TryGetConstant( out _ ) is not true )
{
continue;
}
PropertyList.Add( new SerializedNodeInputElement( properties, node, input, i ) );
}
}
}
}
private void AddSwitchNodeProperties( Properties properties, EditorNode node )
{
var valueType = GetSwitchValueType( node.Node ) ?? typeof( string );
var sPropType = typeof( SerializedSwitchCasesProperty<> ).MakeGenericType( valueType );
PropertyList.Add( (SerializedProperty)Activator.CreateInstance( sPropType, properties, node ) );
}
private Type GetSwitchValueType( Node node )
{
if ( node.Inputs["value"].SourceType is { } valueType )
{
return valueType;
}
if ( node.Properties["cases"].Value is not { } casesObj )
{
return null;
}
foreach ( var iFace in casesObj.GetType().GetInterfaces() )
{
if ( !iFace.IsConstructedGenericType ) continue;
if ( iFace.GetGenericTypeDefinition() != typeof( IReadOnlyList<> ) ) continue;
return iFace.GetGenericArguments()[0];
}
return null;
}
}
public interface ISerializedNodeParameter
{
Node.IParameter Parameter { get; }
}
internal abstract class SerializedNodeParameter<T> : SerializedProperty, ISerializedNodeParameter
where T : Node.IParameter
{
public Properties Properties { get; }
public EditorNode Node { get; }
public T Target { get; }
public override string Name => Target.Name;
public override string DisplayName => Target.Display.Title;
public override string Description => Target.Display.Description;
public override Type PropertyType => Target.Type == typeof( object )
? Properties.PredictBestType( Target )
: Nullable.GetUnderlyingType( Target.Type ) ?? Target.Type;
public abstract object Value { get; set; }
public abstract object DefaultValue { get; }
public abstract bool IsRequired { get; }
public abstract bool HasValue { get; }
Node.IParameter ISerializedNodeParameter.Parameter => Target;
public override IEnumerable<Attribute> GetAttributes() => Target.Attributes.Union( new[] { new AllowNullAttribute() } );
protected SerializedNodeParameter( Properties properties, EditorNode node, T target )
{
Properties = properties;
Node = node;
Target = target;
}
public override bool TryGetAsObject( out SerializedObject obj )
{
obj = null;
var description = EditorTypeLibrary.GetType( PropertyType );
if ( description == null )
{
return false;
}
try
{
if ( !PropertyType.IsValueType )
{
var curVal = GetValue<object>();
if ( curVal == null )
{
return false;
}
obj = EditorTypeLibrary.GetSerializedObject( curVal );
return true;
}
obj = EditorTypeLibrary.GetSerializedObject( () => HasValue && Value is not null ? Value : IsRequired || DefaultValue is null
? Activator.CreateInstance( PropertyType )
: DefaultValue,
description, this );
return true;
}
catch ( Exception e )
{
Log.Warning( e );
obj = null;
return false;
}
}
private static object ConvertTo( object value, Type sourceType, Type targetType )
{
if ( sourceType == targetType || sourceType.IsAssignableTo( targetType ) )
{
return value;
}
if ( Nullable.GetUnderlyingType( targetType ) is { } underlyingTargetType && Nullable.GetUnderlyingType( sourceType ) is null )
{
if ( value is null )
{
return Activator.CreateInstance( targetType, null );
}
return Activator.CreateInstance( targetType, ConvertTo( value, sourceType, underlyingTargetType ) );
}
if ( sourceType == typeof( long ) && targetType.IsEnum )
{
// Special case for EnumControlWidget :S
return Enum.ToObject( targetType, (long)value );
}
if ( sourceType.IsEnum && targetType == typeof( long ) )
{
// Special case for EnumControlWidget :S
return Convert.ChangeType(
Convert.ChangeType(
value,
Enum.GetUnderlyingType( sourceType ) ),
targetType );
}
return Convert.ChangeType( value, targetType );
}
public override void SetValue<TVal>( TVal value )
{
if ( Either.IsEitherType( PropertyType ) )
{
Value = value;
}
else
{
try
{
Value = ConvertTo( value, typeof( TVal ), PropertyType );
}
catch
{
Value = value;
}
}
Node.MarkDirty();
}
public override TVal GetValue<TVal>( TVal defaultValue = default )
{
var rawValue = HasValue ? Value : Target.Definition.IsRequired ? defaultValue : DefaultValue;
if ( rawValue is TVal value )
{
return value;
}
if ( rawValue is null )
{
return defaultValue;
}
try
{
return (TVal)ConvertTo( rawValue, rawValue.GetType(), typeof( TVal ) );
}
catch
{
return defaultValue;
}
}
}
internal class SerializedNodeProperty : SerializedNodeParameter<Node.Property>
{
public override string GroupName => "Properties";
public SerializedNodeProperty( Properties properties, EditorNode node, Node.Property target ) : base( properties, node, target ) { }
public override object Value
{
get => Target.Value;
set => Target.Value = value;
}
public override Type PropertyType => Target.Type;
public override object DefaultValue => Target.Definition.IsRequired ? null : Target.Definition.Default;
public override bool HasValue => Target.Value is not null;
public override bool IsRequired => Target.Definition.IsRequired;
}
internal class SerializedSwitchCasesProperty<T> : SerializedNodeProperty
{
public override Type PropertyType { get; }
private List<T> _value;
public override object Value
{
get
{
if ( _value is not null ) return _value;
if ( base.Value is not IReadOnlyList<T> baseValue )
{
return _value = new List<T>( (IReadOnlyList<T>)base.DefaultValue );
}
if ( baseValue is List<T> list )
{
return _value = list;
}
return _value = new List<T>( baseValue );
}
set
{
_value = (List<T>)value;
base.Value = _value.ToArray();
}
}
public override bool HasValue => true;
public SerializedSwitchCasesProperty( Properties properties, EditorNode node )
: base( properties, node, node.Node.Properties["cases"] )
{
PropertyType = typeof( List<> ).MakeGenericType( typeof( T ) );
}
public override bool TryGetAsObject( out SerializedObject obj )
{
obj = EditorTypeLibrary.GetSerializedObject( Value );
obj.ParentProperty = this;
obj.OnPropertyChanged += prop =>
{
Node.Node.Properties["cases"].Value = null;
Node.Node.Properties["cases"].Value = ((List<T>)Value).ToArray();
Node.MarkDirty();
};
return true;
}
}
internal class SerializedNodeInput : SerializedNodeParameter<Node.Input>
{
public override string GroupName => "Inputs";
public SerializedNodeInput( Properties properties, EditorNode node, Node.Input target ) : base( properties, node, target ) { }
public override object Value
{
get => Target.Value;
set => Target.Value = value;
}
public override object DefaultValue => Target.Definition.IsRequired ? null : Target.Definition.Default;
public override bool HasValue => Target.Link?.TryGetConstant( out _ ) is true;
public override bool IsRequired => Target.Definition.IsRequired;
}
internal class SerializedNodeInputElement : SerializedNodeParameter<Node.Input>
{
public int Index { get; }
public override string DisplayName => $"{Target.Display.Title}[{Index}]";
public override string GroupName => "Inputs";
public SerializedNodeInputElement( Properties properties, EditorNode node, Node.Input target, int index ) : base( properties, node, target )
{
Index = index;
}
public override object Value
{
get => Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count
&& linkArray[Index].TryGetConstant( out var value ) ? value : null;
set
{
if ( Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count )
{
Target.SetLink( new Constant( value ), Index );
}
else
{
Target.InsertLink( new Constant( value ), Index );
}
}
}
public override Type PropertyType => Target.ElementType == typeof( object ) ? Properties.PredictBestType( Target ) : Target.ElementType;
public override object DefaultValue => Target.ElementType.IsValueType
? Activator.CreateInstance( Target.ElementType )
: null;
public override bool HasValue => Target.LinkArray is { } linkArray && Index >= 0 && Index < linkArray.Count
&& linkArray[Index].TryGetConstant( out _ );
public override bool IsRequired => false;
}