Editor/Inspector/BackgroundImageControlWidget.cs
using System;
using System.Linq;
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;
namespace Grains.RazorDesigner.Inspector;
[CustomEditor( typeof( string ), WithAllAttributes = new[] { typeof( BackgroundImagePickerAttribute ) } )]
public sealed class BackgroundImageControlWidget : ControlWidget
{
private const string LogPrefix = "[Grains.RazorDesigner]";
public override bool SupportsMultiEdit => true;
private readonly LineEdit _textEdit;
// Synchronous change events both directions; without this SetValue would re-enter.
private bool _syncing;
public BackgroundImageControlWidget( SerializedProperty property ) : base( property )
{
Log.Info( $"{LogPrefix} BackgroundImageControlWidget ctor for {property.Name}" );
AcceptDrops = true;
Layout = Layout.Row();
Layout.Spacing = 2;
_textEdit = new LineEdit( this )
{
PlaceholderText = "url(\"ui/bg.png\") or linear-gradient(...)",
};
_textEdit.MinimumSize = new Vector2( 60, Theme.RowHeight );
_textEdit.MaximumSize = new Vector2( 4096, Theme.RowHeight );
_textEdit.SetStyles( "background-color: transparent;" );
_textEdit.AcceptDrops = false;
_textEdit.EditingStarted += OnEditStarted;
_textEdit.TextEdited += OnTextEdited;
_textEdit.EditingFinished += OnEditFinished;
Layout.Add( _textEdit, 1 );
var pick = new IconButton( "image", OpenPicker )
{
ToolTip = "Pick an image asset — inserts url(\"…\")",
Background = Color.Transparent,
FixedSize = new Vector2( Theme.RowHeight, Theme.RowHeight ),
};
Layout.Add( pick );
SyncFromProperty();
}
private void SyncFromProperty()
{
if ( _syncing ) return;
_syncing = true;
try
{
if ( SerializedProperty.IsMultipleDifferentValues )
{
if ( !_textEdit.IsFocused ) _textEdit.Text = "";
return;
}
var v = SerializedProperty.GetValue<string>( "" ) ?? "";
// Don't clobber the user's in-progress typing.
if ( !_textEdit.IsFocused && _textEdit.Text != v )
_textEdit.Text = v;
}
finally
{
_syncing = false;
}
}
private void OnEditStarted()
{
if ( ReadOnly || !SerializedProperty.IsEditable ) return;
PropertyStartEdit();
}
private void OnTextEdited( string text )
{
if ( _syncing ) return;
if ( ReadOnly || !SerializedProperty.IsEditable ) return;
Commit( text );
}
private void OnEditFinished()
{
if ( _syncing ) return;
if ( !ReadOnly && SerializedProperty.IsEditable )
Commit( _textEdit.Text );
PropertyFinishEdit();
}
private void Commit( string text )
{
var current = SerializedProperty.GetValue<string>( "" ) ?? "";
if ( text == current ) return;
_syncing = true;
try
{
SerializedProperty.SetValue( text );
SignalValuesChanged();
}
finally
{
_syncing = false;
}
}
// External set (picker / drop) — wrap in its own undo bracket and refresh the text box.
private void SetExternal( string text )
{
if ( ReadOnly || !SerializedProperty.IsEditable ) return;
PropertyStartEdit();
_syncing = true;
try
{
SerializedProperty.SetValue( text );
SignalValuesChanged();
_textEdit.Text = text;
}
finally
{
_syncing = false;
}
PropertyFinishEdit();
Log.Info( $"{LogPrefix} BackgroundImageControlWidget set: {text}" );
}
private static string ToUrl( string relativePath ) => $"url(\"{relativePath}\")";
private void OpenPicker()
{
if ( ReadOnly ) return;
var picker = AssetPicker.Create( this, AssetType.ImageFile );
picker.Title = "Select Background Image";
picker.OnAssetPicked = ( assets ) =>
{
var asset = assets.FirstOrDefault();
if ( asset is not null ) SetExternal( ToUrl( asset.RelativePath ) );
};
picker.Show();
}
public override void OnDragHover( DragEvent ev )
{
// Must set ev.Action here or the drop is never offered — keep it cheap (fires per move).
if ( !ReadOnly && TryResolveImageRelPath( ev, out _ ) )
ev.Action = DropAction.Link;
}
public override void OnDragDrop( DragEvent ev )
{
if ( TryResolveImageRelPath( ev, out var rel ) )
{
Log.Info( $"{LogPrefix} BackgroundImageControlWidget drop -> {rel}" );
SetExternal( ToUrl( rel ) );
ev.Action = DropAction.Link;
return;
}
Log.Info( $"{LogPrefix} BackgroundImageControlWidget drop ignored — not an image asset (Text='{ev.Data.Text}', HasFileOrFolder={ev.Data.HasFileOrFolder}, FileOrFolder='{(ev.Data.HasFileOrFolder ? ev.Data.FileOrFolder : "")}')" );
}
private static bool TryResolveImageRelPath( DragEvent ev, out string relativePath )
{
relativePath = null;
var text = ev.Data.Text;
if ( !string.IsNullOrEmpty( text ) && !text.StartsWith( "file:", StringComparison.OrdinalIgnoreCase ) )
{
var byText = AssetSystem.FindByPath( text );
if ( IsImageAsset( byText ) ) { relativePath = byText.RelativePath; return true; }
}
if ( ev.Data.HasFileOrFolder )
{
var path = ev.Data.FileOrFolder;
if ( !string.IsNullOrEmpty( path ) )
{
var byPath = AssetSystem.FindByPath( path ) ?? AssetSystem.FindByPath( path.Replace( '\\', '/' ) );
if ( IsImageAsset( byPath ) ) { relativePath = byPath.RelativePath; return true; }
}
}
return false;
}
private static bool IsImageAsset( Asset asset )
=> asset is not null && !asset.IsDeleted
&& ( asset.AssetType == AssetType.ImageFile || asset.AssetType == AssetType.Texture );
protected override void OnValueChanged()
{
base.OnValueChanged();
SyncFromProperty();
}
}