using Facepunch.ActionGraphs;
using System;
using System.Reflection;
namespace Editor;
///
/// A control widget is used to edit the value of a single SerializedProperty.
///
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; }
///
/// If none, when in a grid, the control will fill the entire cell
///
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() )
base.ReadOnly = true;
if ( property.TryGetAttribute( out var tintAttribute ) )
{
Tint = Theme.GetTint( tintAttribute.Tint );
}
}
internal ControlWidget() : base( null )
{
HorizontalSizeMode = SizeMode.CanShrink;
VerticalSizeMode = SizeMode.CanGrow;
}
///
/// 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.
///
public virtual void StartEditing()
{
Focus();
}
///
/// If true we prefer to be full inspector width
/// with the label above us
///
public virtual bool IsWideMode => false;
///
/// If true (default) we'll include a label next to the control
///
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( 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( 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() is not null ) return null;
var foundType = EditorTypeLibrary.GetType( "GenericControlWidget" );
if ( foundType is null ) return default;
var w = foundType.Create( new[] { property } );
return w;
}
protected virtual int ValueHash => HashCode.Combine( this, SerializedProperty?.GetValue