Editor/Inspector/ImageSourceControlWidget.cs
using System;
using System.Linq;
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;
namespace Grains.RazorDesigner.Inspector;
[CustomEditor( typeof( string ), WithAllAttributes = new[] { typeof( ImagePickerAttribute ) } )]
public sealed class ImageSourceControlWidget : ControlWidget
{
private const string LogPrefix = "[Grains.RazorDesigner]";
public override bool IsControlButton => true;
public override bool SupportsMultiEdit => true;
public ImageSourceControlWidget( SerializedProperty property ) : base( property )
{
Cursor = CursorShape.Finger;
MouseTracking = true;
AcceptDrops = true;
IsDraggable = true;
OnValueChanged();
}
protected override void PaintControl()
{
var resource = SerializedProperty.GetValue<string>( null );
var asset = !string.IsNullOrEmpty( resource ) ? AssetSystem.FindByPath( resource ) : null;
var rect = new Rect( 0, Size );
var iconRect = rect.Shrink( 2 );
iconRect.Width = iconRect.Height;
rect.Left = iconRect.Right + 10;
Paint.ClearPen();
Paint.SetBrush( Theme.SurfaceBackground.WithAlpha( 0.2f ) );
Paint.DrawRect( iconRect, 2 );
Pixmap fallbackIcon = AssetType.ImageFile?.Icon64;
if ( SerializedProperty.IsMultipleDifferentValues )
{
if ( fallbackIcon != null ) Paint.Draw( iconRect, fallbackIcon );
Paint.SetDefaultFont();
Paint.SetPen( Theme.MultipleValues );
Paint.DrawText( rect.Shrink( 0, 3 ), "Multiple Values", TextFlag.LeftCenter );
return;
}
if ( asset is not null && !asset.IsDeleted )
{
Paint.Draw( iconRect, asset.GetAssetThumb( true ) );
var textRect = rect.Shrink( 0, 3 );
Paint.SetPen( Theme.Text.WithAlpha( 0.9f ) );
Paint.SetHeadingFont( 8, 450 );
var t = Paint.DrawText( textRect, asset.Name, TextFlag.LeftTop );
textRect.Left = t.Right + 6;
Paint.SetDefaultFont( 7 );
Theme.DrawFilename( textRect, asset.RelativePath, TextFlag.LeftCenter, Theme.Text.WithAlpha( 0.5f ) );
return;
}
if ( !string.IsNullOrWhiteSpace( resource ) )
{
Paint.SetBrush( Theme.Red.Darken( 0.8f ) );
Paint.DrawRect( iconRect, 2 );
Paint.SetPen( Theme.Red );
Paint.DrawIcon( iconRect, "error", Math.Max( 16, iconRect.Height / 2 ) );
var textRect = rect.Shrink( 0, 3 );
Paint.SetPen( Theme.Text.WithAlpha( 0.9f ) );
Paint.SetHeadingFont( 8, 450 );
var t = Paint.DrawText( textRect, "Missing image", TextFlag.LeftTop );
textRect.Left = t.Right + 6;
Paint.SetDefaultFont( 7 );
Theme.DrawFilename( textRect, resource, TextFlag.LeftCenter, Theme.Text.WithAlpha( 0.5f ) );
return;
}
if ( fallbackIcon != null ) Paint.Draw( iconRect, fallbackIcon );
Paint.SetDefaultFont( italic: true );
Paint.SetPen( Theme.Text.WithAlpha( 0.2f ) );
Paint.DrawText( rect.Shrink( 0, 3 ), "Drop or click to pick image", TextFlag.LeftCenter );
}
protected override void OnContextMenu( ContextMenuEvent e )
{
var m = new ContextMenu();
var resource = SerializedProperty.GetValue<string>( null );
var asset = !string.IsNullOrEmpty( resource ) ? AssetSystem.FindByPath( resource ) : null;
m.AddOption( "Open in Editor", "edit", () => asset?.OpenInEditor() ).Enabled = asset is not null && !asset.IsProcedural;
m.AddOption( "Find in Asset Browser", "search", () => LocalAssetBrowser.OpenTo( asset, true ) ).Enabled = asset is not null;
m.AddSeparator();
m.AddOption( "Copy", "file_copy", Copy ).Enabled = asset is not null;
m.AddOption( "Paste", "content_paste", Paste );
m.AddSeparator();
m.AddOption( "Clear", "backspace", Clear ).Enabled = !string.IsNullOrEmpty( resource );
m.OpenAtCursor( false );
e.Accepted = true;
}
protected override void OnMouseClick( MouseEvent e )
{
base.OnMouseClick( e );
if ( ReadOnly ) return;
OpenPicker();
}
private void OpenPicker()
{
var resource = SerializedProperty.GetValue<string>( null );
var asset = !string.IsNullOrEmpty( resource ) ? AssetSystem.FindByPath( resource ) : null;
var picker = AssetPicker.Create( this, AssetType.ImageFile );
picker.SetSelection( resource );
picker.Title = "Select Image";
picker.OnAssetHighlighted = ( o ) => UpdateFromAsset( o.FirstOrDefault() );
picker.OnAssetPicked = ( o ) => UpdateFromAsset( o.FirstOrDefault() );
picker.Show();
picker.SetSelection( asset );
}
private void UpdateFromAsset( Asset asset )
{
if ( asset is null ) return;
SerializedProperty.Parent?.NoteStartEdit( SerializedProperty );
SerializedProperty.SetValue( asset.RelativePath );
SerializedProperty.Parent?.NoteFinishEdit( SerializedProperty );
Log.Info( $"{LogPrefix} ImageSourceControlWidget set: {asset.RelativePath}" );
}
public override void OnDragHover( DragEvent ev )
{
var asset = ResolveDraggedAsset( ev );
if ( IsAcceptable( asset ) ) { ev.Action = DropAction.Link; return; }
if ( TryResolveRawImagePath( ev, out _ ) ) { ev.Action = DropAction.Link; return; }
}
public override void OnDragDrop( DragEvent ev )
{
var asset = ResolveDraggedAsset( ev );
if ( IsAcceptable( asset ) )
{
PropertyStartEdit();
UpdateFromAsset( asset );
PropertyFinishEdit();
ev.Action = DropAction.Link;
return;
}
if ( TryResolveRawImagePath( ev, out var relPath ) )
{
PropertyStartEdit();
SerializedProperty.Parent?.NoteStartEdit( SerializedProperty );
SerializedProperty.SetValue( relPath );
SerializedProperty.Parent?.NoteFinishEdit( SerializedProperty );
PropertyFinishEdit();
Log.Info( $"{LogPrefix} ImageSourceControlWidget set: {relPath}" );
ev.Action = DropAction.Link;
return;
}
}
private static Asset ResolveDraggedAsset( DragEvent ev )
{
var text = ev.Data.Text;
if ( !string.IsNullOrEmpty( text ) && !text.StartsWith( "file:", StringComparison.OrdinalIgnoreCase ) )
{
var byText = AssetSystem.FindByPath( text );
if ( byText is not null ) return byText;
}
if ( ev.Data.HasFileOrFolder )
{
var path = ev.Data.FileOrFolder;
if ( !string.IsNullOrEmpty( path ) )
{
var byPath = AssetSystem.FindByPath( path );
if ( byPath is not null ) return byPath;
var flipped = path.Replace( '\\', '/' );
if ( flipped != path )
{
var byFlipped = AssetSystem.FindByPath( flipped );
if ( byFlipped is not null ) return byFlipped;
}
}
}
return null;
}
private static bool IsAcceptable( Asset asset )
{
if ( asset is null ) return false;
return asset.AssetType == AssetType.ImageFile || asset.AssetType == AssetType.Texture;
}
private static readonly string[] _rawImageExtensions = { ".png", ".jpg", ".jpeg", ".tga", ".exr", ".webp", ".gif" };
private static bool TryResolveRawImagePath( DragEvent ev, out string relativePath )
{
relativePath = null;
if ( !ev.Data.HasFileOrFolder ) return false;
var path = ev.Data.FileOrFolder;
if ( string.IsNullOrEmpty( path ) ) return false;
if ( !System.IO.File.Exists( path ) ) return false;
var ext = System.IO.Path.GetExtension( path ).ToLowerInvariant();
if ( Array.IndexOf( _rawImageExtensions, ext ) < 0 ) return false;
var assetsRoot = Project.Current?.GetAssetsPath();
if ( string.IsNullOrEmpty( assetsRoot ) ) return false;
relativePath = System.IO.Path.GetRelativePath( assetsRoot, path ).Replace( '\\', '/' );
return true;
}
protected override void OnDragStart()
{
var resource = SerializedProperty.GetValue<string>( null );
var asset = !string.IsNullOrEmpty( resource ) ? AssetSystem.FindByPath( resource ) : null;
if ( asset is null ) return;
var drag = new Drag( this );
drag.Data.Url = new Uri( $"file://{asset.AbsolutePath}" );
drag.Execute();
}
private void Copy()
{
var resource = SerializedProperty.GetValue<string>( null );
if ( string.IsNullOrEmpty( resource ) ) return;
var asset = AssetSystem.FindByPath( resource );
if ( asset is not null ) resource = asset.RelativePath;
EditorUtility.Clipboard.Copy( resource );
}
private void Paste()
{
var path = EditorUtility.Clipboard.Paste();
var asset = !string.IsNullOrEmpty( path ) ? AssetSystem.FindByPath( path ) : null;
UpdateFromAsset( asset );
}
private void Clear()
{
SerializedProperty.Parent?.NoteStartEdit( SerializedProperty );
SerializedProperty.SetValue( "" );
SerializedProperty.Parent?.NoteFinishEdit( SerializedProperty );
Log.Info( $"{LogPrefix} ImageSourceControlWidget cleared" );
}
}