mirror of
https://github.com/Facepunch/sbox-public.git
synced 2025-12-30 18:08:13 -05:00
This commit imports the C# engine code and game files, excluding C++ source code. [Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
461 lines
11 KiB
C#
461 lines
11 KiB
C#
using Facepunch.ActionGraphs;
|
|
using System;
|
|
using System.Reflection;
|
|
|
|
namespace Editor;
|
|
|
|
/// <summary>
|
|
/// A control widget is used to edit the value of a single SerializedProperty.
|
|
/// </summary>
|
|
public abstract class ControlWidget : Widget
|
|
{
|
|
static Logger log = new Logger( "ControlWidget" );
|
|
|
|
public static Color ControlHighlightPrimary = "#77BBFF";
|
|
public static Color ControlHighlightSecondary = "#B0E24D";
|
|
|
|
public SerializedProperty SerializedProperty { get; private set; }
|
|
|
|
/// <summary>
|
|
/// If none, when in a grid, the control will fill the entire cell
|
|
/// </summary>
|
|
public virtual TextFlag CellAlignment => TextFlag.None;
|
|
|
|
public ControlWidget( SerializedProperty property ) : this()
|
|
{
|
|
ArgumentNullException.ThrowIfNull( property, "SerializedProperty" );
|
|
|
|
SerializedProperty = property;
|
|
ToolTip = property.Description ?? property.DisplayName;
|
|
MinimumWidth = 200;
|
|
|
|
//
|
|
// This actually makes no real sense. Think of these scenarios
|
|
//
|
|
// -- float value { get; } // yes - don't edit
|
|
// -- Model value { get; } // yes - don't edit
|
|
// -- DataClass value { get; } // class is editable, but not the property
|
|
//
|
|
// I think this should be more explicit, only readonly if there's a [readonly] property
|
|
//
|
|
if ( !property.IsEditable && property.PropertyType.IsValueType || property.HasAttribute<ReadOnlyAttribute>() )
|
|
base.ReadOnly = true;
|
|
|
|
if ( property.TryGetAttribute<TintAttribute>( out var tintAttribute ) )
|
|
{
|
|
Tint = Theme.GetTint( tintAttribute.Tint );
|
|
}
|
|
}
|
|
|
|
internal ControlWidget() : base( null )
|
|
{
|
|
HorizontalSizeMode = SizeMode.CanShrink;
|
|
VerticalSizeMode = SizeMode.CanGrow;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects this widget and starts editing. Used when we want to focus on the widget in the
|
|
/// inspector, like when double-clicking on something in a graph editor that maps to this widget.
|
|
/// </summary>
|
|
public virtual void StartEditing()
|
|
{
|
|
Focus();
|
|
}
|
|
|
|
/// <summary>
|
|
/// If true we prefer to be full inspector width
|
|
/// with the label above us
|
|
/// </summary>
|
|
public virtual bool IsWideMode => false;
|
|
|
|
/// <summary>
|
|
/// If true (default) we'll include a label next to the control
|
|
/// </summary>
|
|
public virtual bool IncludeLabel => true;
|
|
public virtual bool IsControlActive => IsFocused;
|
|
public virtual bool IsControlHovered => IsUnderMouse && Enabled && !ReadOnly;
|
|
public virtual bool IsControlDisabled => !Enabled || ReadOnly;
|
|
public virtual bool IsControlButton => false;
|
|
public Color Tint { get; set; } = Color.White;
|
|
public virtual bool SupportsMultiEdit => false;
|
|
public bool PaintBackground = true;
|
|
|
|
protected override Vector2 MinimumSizeHint()
|
|
{
|
|
return Theme.RowHeight;
|
|
}
|
|
|
|
protected override Vector2 SizeHint()
|
|
{
|
|
var size = base.SizeHint();
|
|
size.x = 1000;
|
|
return size;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
Paint.Antialiasing = true;
|
|
Paint.TextAntialiasing = true;
|
|
|
|
PaintUnder();
|
|
PaintControl();
|
|
PaintOver();
|
|
}
|
|
|
|
protected virtual void PaintUnder()
|
|
{
|
|
if ( !PaintBackground )
|
|
return;
|
|
|
|
bool active = IsControlActive;
|
|
bool hovered = IsControlHovered;
|
|
bool read = IsControlDisabled;
|
|
|
|
Paint.ClearPen();
|
|
|
|
if ( IsControlButton )
|
|
{
|
|
if ( hovered )
|
|
{
|
|
Paint.SetPen( Color.Lerp( Theme.ControlBackground, ControlHighlightPrimary, 0.6f ), 1 );
|
|
Paint.SetBrush( Color.Lerp( Theme.ControlBackground, ControlHighlightPrimary, 0.2f ) );
|
|
Paint.DrawRect( LocalRect.Shrink( 1 ), Theme.ControlRadius );
|
|
return;
|
|
}
|
|
|
|
Paint.SetBrush( Theme.ControlBackground );
|
|
Paint.DrawRect( LocalRect, Theme.ControlRadius );
|
|
return;
|
|
}
|
|
|
|
if ( read )
|
|
{
|
|
Paint.SetBrush( Theme.ControlBackground.Lighten( 0.5f ) );
|
|
}
|
|
else if ( active )
|
|
{
|
|
Paint.SetBrush( Theme.ControlBackground.Darken( 0.3f ) );
|
|
}
|
|
else if ( hovered )
|
|
{
|
|
Paint.SetBrush( Theme.ControlBackground.Darken( 0.2f ) );
|
|
}
|
|
else
|
|
{
|
|
Paint.SetBrush( Theme.ControlBackground );
|
|
}
|
|
|
|
Paint.DrawRect( LocalRect, Theme.ControlRadius );
|
|
}
|
|
|
|
protected virtual void PaintControl()
|
|
{
|
|
|
|
}
|
|
|
|
protected virtual void PaintOver()
|
|
{
|
|
bool active = IsControlActive;
|
|
bool hovered = IsControlHovered;
|
|
|
|
if ( hovered && IsBeingDroppedOn )
|
|
{
|
|
Paint.SetPen( ControlHighlightSecondary.WithAlpha( 0.8f ), 2, PenStyle.Dot );
|
|
Paint.SetBrush( ControlHighlightSecondary.WithAlpha( 0.2f ) );
|
|
Paint.DrawRect( LocalRect.Shrink( 2 ), Theme.ControlRadius );
|
|
return;
|
|
}
|
|
}
|
|
|
|
[EditorEvent.Hotload]
|
|
static void FlushCache()
|
|
{
|
|
editorAttributes = null;
|
|
}
|
|
|
|
static (TypeDescription Type, CustomEditorAttribute Attribute)[] editorAttributes;
|
|
|
|
public static ControlWidget Create( SerializedProperty property )
|
|
{
|
|
if ( property is null )
|
|
{
|
|
return new InvalidPropertyControlWidget();
|
|
}
|
|
|
|
ArgumentNullException.ThrowIfNull( property );
|
|
|
|
var type = property.PropertyType;
|
|
|
|
log.Trace( $"Target Type: {type}" );
|
|
|
|
editorAttributes ??= EditorTypeLibrary.GetTypesWithAttribute<CustomEditorAttribute>( false )
|
|
.Where( x => x.Type.TargetType.IsAssignableTo( typeof( ControlWidget ) ) )
|
|
.ToArray();
|
|
|
|
var allEditors = editorAttributes
|
|
.Select( x => new { score = x.Attribute.GetEditorScore( property ), editor = x } )
|
|
.Where( x => x.score > 0 )
|
|
.OrderByDescending( x => x.score )
|
|
.ToArray();
|
|
|
|
// debug output
|
|
int i = 0;
|
|
foreach ( var entry in allEditors )
|
|
{
|
|
log.Trace( $" {++i}. [{entry.score}]\t{entry.editor.Type.FullName}" );
|
|
}
|
|
|
|
// Use the first editor we can successfully create
|
|
foreach ( var entry in allEditors )
|
|
{
|
|
var c = entry.editor.Type.Create<ControlWidget>( new[] { property } );
|
|
if ( c is not null )
|
|
{
|
|
if ( property.IsMultipleValues && !c.SupportsMultiEdit )
|
|
{
|
|
c.Destroy();
|
|
return new MultiEditNotSupported( property );
|
|
}
|
|
|
|
c.Prime();
|
|
return c;
|
|
}
|
|
}
|
|
|
|
// Nope - sorry, nothing for you
|
|
if ( property.IsMethod )
|
|
return null;
|
|
|
|
var w = TryCreateGenericObjectControlWidget( property );
|
|
if ( w is not null ) return w;
|
|
|
|
return new MissingSerializedPropertyWidget( property );
|
|
}
|
|
|
|
public static ControlWidget TryCreateGenericObjectControlWidget( SerializedProperty property )
|
|
{
|
|
//
|
|
// Is this appropriate for the GenericControlWidget?
|
|
//
|
|
|
|
// primitive, nope
|
|
if ( property.PropertyType.IsPrimitive ) return null;
|
|
|
|
// readonly struct
|
|
if ( property.PropertyType.IsValueType && property.PropertyType.GetCustomAttribute<System.Runtime.CompilerServices.IsReadOnlyAttribute>() is not null ) return null;
|
|
|
|
|
|
var foundType = EditorTypeLibrary.GetType<ControlWidget>( "GenericControlWidget" );
|
|
if ( foundType is null ) return default;
|
|
|
|
var w = foundType.Create<ControlWidget>( new[] { property } );
|
|
|
|
return w;
|
|
}
|
|
|
|
protected virtual int ValueHash => HashCode.Combine( this, SerializedProperty?.GetValue<object>() );
|
|
|
|
[EditorEvent.Frame]
|
|
public virtual void Think()
|
|
{
|
|
if ( !Visible )
|
|
return;
|
|
|
|
if ( SetContentHash( ValueHash, 0.1f ) )
|
|
{
|
|
OnValueChanged();
|
|
CheckDifferentState();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Should get called right after creation
|
|
/// </summary>
|
|
public void Prime()
|
|
{
|
|
SetContentHash( ValueHash, -1000.0f );
|
|
OnValueChanged();
|
|
UpdateDifferentState();
|
|
}
|
|
|
|
protected void PropertyStartEdit()
|
|
{
|
|
SerializedProperty.NoteStartEdit( SerializedProperty );
|
|
}
|
|
|
|
protected void PropertyFinishEdit()
|
|
{
|
|
SerializedProperty.NoteFinishEdit( SerializedProperty );
|
|
}
|
|
|
|
protected virtual void OnValueChanged()
|
|
{
|
|
|
|
}
|
|
|
|
protected override void OnContextMenu( ContextMenuEvent e )
|
|
{
|
|
e.Accepted = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ActionGraph serializer needs hints about the GameObject that contains this property.
|
|
/// </summary>
|
|
private IDisposable PushSerializationOptions()
|
|
{
|
|
if ( SerializedProperty.GetContainingGameObject() is { } gameObject )
|
|
{
|
|
return ActionGraph.PushTarget( InputDefinition.Target( typeof( GameObject ), gameObject ) );
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual string ToClipboardString()
|
|
{
|
|
using var optionScope = PushSerializationOptions();
|
|
|
|
string str = Json.Serialize( SerializedProperty.GetValue<object>() );
|
|
if ( str?.StartsWith( '"' ) ?? false ) str = str.Substring( 1, str.Length - 2 );
|
|
return str;
|
|
}
|
|
|
|
public virtual void FromClipboardString( string clipboard )
|
|
{
|
|
clipboard = clipboard.Trim();
|
|
|
|
if ( !clipboard.StartsWith( '{' ) && !clipboard.StartsWith( '[' ) && !clipboard.StartsWith( '"' ) )
|
|
clipboard = $"\"{clipboard}\"";
|
|
|
|
using var optionScope = PushSerializationOptions();
|
|
using var uniqueActionGuidScope = ActionGraph.PushMakeGuidsUnique( true );
|
|
|
|
if ( Json.TryDeserialize( clipboard, SerializedProperty.PropertyType, out var jsonValue ) && jsonValue is not null )
|
|
{
|
|
SerializedProperty.Parent.NoteStartEdit( SerializedProperty );
|
|
SerializedProperty.SetValue( jsonValue );
|
|
SerializedProperty.Parent.NoteFinishEdit( SerializedProperty );
|
|
}
|
|
}
|
|
|
|
bool _multipleDifferent;
|
|
|
|
void CheckDifferentState()
|
|
{
|
|
if ( SerializedProperty is null ) // only possible in InvalidPropertyControlWidget
|
|
return;
|
|
|
|
if ( _multipleDifferent == SerializedProperty.IsMultipleDifferentValues )
|
|
return;
|
|
|
|
UpdateDifferentState();
|
|
}
|
|
|
|
void UpdateDifferentState()
|
|
{
|
|
_multipleDifferent = SerializedProperty.IsMultipleDifferentValues;
|
|
OnMultipleDifferentValues( _multipleDifferent );
|
|
}
|
|
|
|
protected virtual void OnMultipleDifferentValues( bool state )
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used when there's no defined ControlWidget
|
|
/// </summary>
|
|
file class MissingSerializedPropertyWidget : ControlWidget
|
|
{
|
|
public MissingSerializedPropertyWidget( SerializedProperty property ) : base( property )
|
|
{
|
|
ReadOnly = true;
|
|
}
|
|
|
|
protected override void OnValueChanged()
|
|
{
|
|
base.OnValueChanged();
|
|
|
|
UpdateGeometry();
|
|
}
|
|
|
|
protected override Vector2 SizeHint()
|
|
{
|
|
var text = SerializedProperty.GetValue( "Missing Value" );
|
|
var sh = base.SizeHint();
|
|
sh.x -= 16;
|
|
var rect = Paint.MeasureText( new Rect( 0, sh ), text, TextFlag.LeftTop );
|
|
|
|
if ( rect.Height < Theme.RowHeight )
|
|
rect.Height = Theme.RowHeight;
|
|
|
|
return rect.Size;
|
|
}
|
|
|
|
protected override void OnPaint()
|
|
{
|
|
var text = SerializedProperty.GetValue( "Missing Value" );
|
|
Paint.SetPen( Theme.TextControl.WithAlpha( 0.5f ) );
|
|
Paint.DrawText( LocalRect.Shrink( 8, 0 ), text, TextFlag.LeftCenter );
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used when there's no defined ControlWidget
|
|
/// </summary>
|
|
file class InvalidPropertyControlWidget : ControlWidget
|
|
{
|
|
Label Label;
|
|
|
|
public InvalidPropertyControlWidget() : base()
|
|
{
|
|
ReadOnly = true;
|
|
HorizontalSizeMode = SizeMode.Flexible;
|
|
|
|
Label = new Label( "Null Property" );
|
|
Label.WordWrap = true;
|
|
Label.SetStyles( "background-color: transparent;" );
|
|
Label.ContentMargins = new Sandbox.UI.Margin( 6, 0 );
|
|
Label.FixedHeight = Theme.RowHeight;
|
|
|
|
Layout = Layout.Column();
|
|
Layout.Add( Label );
|
|
}
|
|
|
|
protected override Vector2 SizeHint() => new Vector2( 22, Theme.RowHeight );
|
|
|
|
protected override void OnValueChanged()
|
|
{
|
|
base.OnValueChanged();
|
|
|
|
Label.Text = "Null Property";
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Used when there's no defined ControlWidget
|
|
/// </summary>
|
|
file class MultiEditNotSupported : ControlWidget
|
|
{
|
|
Label Label;
|
|
|
|
public MultiEditNotSupported( SerializedProperty property ) : base( property )
|
|
{
|
|
ReadOnly = true;
|
|
HorizontalSizeMode = SizeMode.Flexible;
|
|
|
|
Label = new Label( "Multiedit not supported", this );
|
|
Label.WordWrap = true;
|
|
Label.SetStyles( "background-color: transparent;" );
|
|
Label.ContentMargins = new Sandbox.UI.Margin( 6, 0 );
|
|
Label.MaximumSize = new Vector2( 4096, Theme.RowHeight );
|
|
|
|
Layout = Layout.Column();
|
|
Layout.Add( Label );
|
|
}
|
|
|
|
protected override Vector2 SizeHint() => new Vector2( 22, Theme.RowHeight );
|
|
}
|