using Editor.AssetPickers; /// /// Shows a control for editing embedded resources. This is basically a dropdown, when you click on it /// the popup editor will show up with the properties of the embedded resource - with a toolbar. /// public class EmbeddedResourceControlWidget : ControlWidget { TypeDescription _typeDescription; AssetType _assetType; Layout _toolbarRow; Widget _toolbar; protected StickyPopup _popup; public override bool SupportsMultiEdit => true; public override bool IsControlButton => !IsControlDisabled; Resource Resource => SerializedProperty.GetValue( null ); /// /// A list of supported types (if any) that we can switch this embedded resource to. /// List SupportedTypes { get { return EditorTypeLibrary.GetTypes( SerializedProperty.PropertyType ) .Where( x => x.TargetType.IsPublic && !x.IsAbstract && x.TargetType.IsSubclassOf( typeof( GameResource ) ) ) .Where( x => x.TargetType != Resource?.GetType() ) .OrderBy( x => x.Name ) .ToList(); } } public EmbeddedResourceControlWidget( SerializedProperty property ) : base( property ) { HorizontalSizeMode = SizeMode.CanGrow | SizeMode.Expand; VerticalSizeMode = SizeMode.CanGrow; Layout = Layout.Column(); Cursor = CursorShape.Finger; // // If we're opening the popup editor, we need to ensure that the resource is valid // var resource = Resource; if ( resource is null && !SerializedProperty.PropertyType.IsAbstract ) { resource = EditorTypeLibrary.Create( SerializedProperty.PropertyType.Name ); SerializedProperty.SetValue( resource ); } SerializedProperty.OnChanged = OnChanged; Reset( resource, SerializedProperty ); } /// /// Resets the resource's embed info to default /// /// /// private void Reset( Resource resource, SerializedProperty property ) { // // This can be null if the resource's type is abstract // if ( resource.IsValid() ) { var type = Game.TypeLibrary.GetType( resource.GetType() ); string typeName = null; if ( !property.PropertyType.Equals( type.TargetType ) ) { typeName = type.Name; } resource.EmbeddedResource = new() { ResourceCompiler = "embed", TypeName = typeName }; } _typeDescription = Game.TypeLibrary.GetType( resource.IsValid() ? resource.GetType() : property.PropertyType ); _assetType = AssetType.FromType( _typeDescription?.TargetType ?? property.PropertyType ); } void OnChanged( SerializedProperty prop ) { // Rebuild popup if external changes were made if ( _popup.IsValid() ) { BuildPopup( _popup ); } } protected override void OnMouseReleased( MouseEvent e ) { if ( _popup.IsValid() ) { _popup?.Destroy(); _popup = null; return; } OpenPopupEditor(); } public override void OnDestroyed() { _popup?.Destroy(); _popup = null; base.OnDestroyed(); } protected override void OnPaint() { var asset = Resource.IsValid() ? AssetSystem.FindByPath( Resource.ResourcePath ) : null; var pickerName = DisplayInfo.ForType( SerializedProperty.PropertyType ).Name; if ( _assetType is not null ) pickerName = _assetType.FriendlyName; var text = $"Embedded {pickerName}"; if ( SerializedProperty.IsMultipleDifferentValues ) { text = $"Multiple Values"; } if ( _assetType?.Icon64 is Pixmap icon ) { Theme.DrawDropdown( LocalRect, text, "", _popup.IsValid(), IsControlDisabled || !Resource.IsValid() ); Paint.Draw( new Rect( 2, Height - 4 ), icon, Paint.HasMouseOver ? 1f : 0.7f ); } else { Theme.DrawDropdown( LocalRect, text, "edit_note", _popup.IsValid(), IsControlDisabled ); } } void Reset() { var target = SerializedProperty.GetValue(); if ( !target.IsValid() ) return; PropertyStartEdit(); // // Clean slate // var newResource = EditorTypeLibrary.Create( SerializedProperty.PropertyType.Name ); Reset( newResource, SerializedProperty ); SerializedProperty.SetValue( newResource ); SignalValuesChanged(); PropertyFinishEdit(); } void Copy() { string str = ToClipboardString(); EditorUtility.Clipboard.Copy( str ); } void Paste() { PropertyStartEdit(); string str = EditorUtility.Clipboard.Paste(); FromClipboardString( str ); BuildPopup( _popup ); SignalValuesChanged(); PropertyStartEdit(); } void SetType( TypeDescription type ) { PropertyStartEdit(); var resource = EditorTypeLibrary.Create( type.Name ); Reset( resource, SerializedProperty ); SerializedProperty.SetValue( resource ); BuildPopup( _popup ); SignalValuesChanged(); PropertyFinishEdit(); } void OpenTypePopup() { var menu = new ContextMenu( _popup ); var resourceType = Resource?.GetType() ?? SerializedProperty.PropertyType; foreach ( var type in SupportedTypes ) { var isCurrent = type.TargetType.Equals( resourceType ); var item = menu.AddOption( type.Title, action: () => SetType( type ) ); item.Icon = type.Icon ?? "category"; item.Enabled = !isCurrent; } menu.OpenAt( _toolbar.ScreenRect.BottomLeft ); } void BuildPopup( StickyPopup popup ) { if ( !popup.IsValid() ) return; // // Clear the layout // popup.Layout.Clear( true ); popup.OnPaintOverride = PaintPopupBackground; // // ReadOnly // popup.ReadOnly = !SerializedProperty.IsEditable; // // Toolbar // _toolbarRow = popup.Layout.AddRow(); _toolbarRow.Spacing = 4; var toolbar = new ToolBar( popup ); toolbar.SetIconSize( 13 ); toolbar.ButtonStyle = ToolButtonStyle.TextUnderIcon; if ( SupportedTypes.Any() ) { toolbar.AddOption( "Type", "type_specimen", action: OpenTypePopup ).Enabled = !popup.ReadOnly; } var so = Resource?.GetSerialized(); if ( so.IsValid() ) { if ( _assetType?.HasEditor ?? false ) { toolbar.AddOption( "Editor", "dvr", action: OpenInEditor ).Enabled = !popup.ReadOnly; } toolbar.AddSeparator(); toolbar.AddOption( "Save As", "drive_file_move", action: ConvertToFile ).Enabled = !popup.ReadOnly; toolbar.AddOption( "Load", "swap_horiz", action: LoadFromFile ).Enabled = !popup.ReadOnly; toolbar.AddSeparator(); toolbar.AddOption( "Copy", "content_copy", action: Copy ); toolbar.AddOption( "Paste", "content_paste", action: Paste ).Enabled = !popup.ReadOnly; toolbar.AddSeparator(); toolbar.AddOption( "Clear", "delete", action: Reset ).Enabled = !popup.ReadOnly; } _toolbar = toolbar; _toolbarRow.Add( toolbar ); // // Populate control sheet with properties // popup.CreateProperties( so ); } bool PaintPopupBackground() { // Body Paint.ClearPen(); Paint.SetBrushLinear( 0, Vector2.Down * 256, Theme.SurfaceBackground.Lighten( 0.2f ).WithAlpha( 0.98f ), Theme.SurfaceBackground.WithAlpha( 0.95f ) ); Paint.DrawRect( Paint.LocalRect ); // Toolbar var toolbarColor = SerializedProperty.IsEditable ? Theme.Green : Theme.Green.Desaturate( 0.5f ); Paint.ClearPen(); Paint.SetBrush( toolbarColor.WithAlpha( 0.1f ) ); Paint.DrawRect( _toolbarRow.OuterRect ); Paint.ClearBrush(); Paint.SetPen( Color.Black.WithAlpha( 0.33f ), 2, PenStyle.Solid ); Paint.DrawRect( Paint.LocalRect.Shrink( 0, -10, 1, 1 ), 4 ); return true; } protected void OpenPopupEditor() { _popup?.Destroy(); _popup = null; var popup = new StickyPopup( null ) { Owner = this, MinimumWidth = Width, Position = ScreenRect.BottomLeft }; BuildPopup( popup ); popup.Visible = true; popup.Focus( true ); _popup = popup; _popup.DestroyUnrelatedPopups(); } private async void ConvertToFile() { var fd = new FileDialog( null ); fd.Title = "Convert embedded resource to file.."; fd.DefaultSuffix = _assetType.FileExtension; fd.Directory = Project.Current.GetAssetsPath(); fd.SelectFile( $"resource.{_assetType.FileExtension}" ); fd.SetFindFile(); fd.SetModeSave(); fd.SetNameFilter( $"Resource (*.{_assetType.FileExtension})" ); if ( !fd.Execute() ) return; var asset = AssetSystem.CreateResource( _assetType.FileExtension, fd.SelectedFile ); await asset.CompileIfNeededAsync(); if ( asset.TryLoadResource( out var gr ) ) { var embeddedSerialized = SerializedProperty.GetValue().Serialize(); // Deserialize into this freshly made resource gr.Deserialize( embeddedSerialized ); // Cull any notion of an embedded resource, as it's not one anymore gr.EmbeddedResource = null; asset.SaveToDisk( gr ); } // Link to the GameResource SerializedProperty.SetValue( gr ); MainAssetBrowser.Instance?.Local.UpdateAssetList(); _popup?.Destroy(); _popup = null; } void LoadFromFile() { var picker = new ResourcePicker( Parent, _assetType ); var query = $"t:{_assetType.FileExtension}"; picker.OnAssetPicked += x => { var asset = x.FirstOrDefault(); if ( asset?.TryLoadResource( out var gr ) ?? false ) { CreateEmbeddedFromFile( SerializedProperty, gr ); } BuildPopup( _popup ); }; picker.Title = $"Select {SerializedProperty.DisplayName}"; picker.Show(); } public static EmbeddedResourceControlWidget CreateWidget( SerializedProperty target ) { var customType = EditorTypeLibrary.GetTypes( typeof( EmbeddedResourceControlWidget ) ) .FirstOrDefault( x => { if ( !x.TargetType.IsAssignableTo( typeof( EmbeddedResourceControlWidget ) ) ) return false; var attr = x.GetAttribute(); if ( attr is null ) return false; return attr.TargetType == target.PropertyType; } ); if ( customType is null ) return new EmbeddedResourceControlWidget( target ); return EditorTypeLibrary.Create( customType.FullName, [target] ); } /// /// Creates a new embedded resource from the given file resource. /// /// /// /// public static GameResource CreateEmbeddedFromFile( SerializedProperty target, GameResource source ) { if ( target.PropertyType.IsAbstract ) return null; var data = source.Serialize(); // // Create a fresh resource when switching to embedded // var resource = EditorTypeLibrary.Create( target.PropertyType.Name ); resource.Deserialize( data ); resource.EmbeddedResource = new() { ResourceCompiler = "embed", Data = data }; target.SetValue( resource ); return resource; } void OpenInEditor() { IAssetEditor.OpenInEditor( AssetSystem.CreateEmbeddedAsset( SerializedProperty ), out var editor ); _popup?.Destroy(); _popup = null; } } file class ResourcePicker : SimplePicker { public ResourcePicker( Widget parent, AssetType assetType ) : base( parent, assetType, new() ) { Title = $"Select {assetType.FriendlyName}"; } }