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() ); [EditorEvent.Frame] public virtual void Think() { if ( !Visible ) return; if ( SetContentHash( ValueHash, 0.1f ) ) { OnValueChanged(); CheckDifferentState(); } } /// /// Should get called right after creation /// 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; } /// /// ActionGraph serializer needs hints about the GameObject that contains this property. /// 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() ); 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 ) { } } /// /// Used when there's no defined ControlWidget /// 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 ); } } /// /// Used when there's no defined ControlWidget /// 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"; } } /// /// Used when there's no defined ControlWidget /// 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 ); }