From 754ed56119de2847deb4210ed8f1fd8c63d65da3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 3 May 2024 12:26:03 +0200 Subject: [PATCH] Add `FilePathEditor` custom editor for path-based editing asset/url refs with a file picker --- Source/Editor/Content/AssetPickerValidator.cs | 30 +-- .../CustomEditors/Editors/AssetRefEditor.cs | 215 +++++++++++++++--- Source/Editor/GUI/AssetPicker.cs | 2 - Source/Editor/Utilities/Utils.cs | 22 ++ Source/Engine/Video/VideoPlayer.h | 2 +- 5 files changed, 212 insertions(+), 59 deletions(-) diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs index f43ab6a29..109ea2f79 100644 --- a/Source/Editor/Content/AssetPickerValidator.cs +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -1,3 +1,5 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + using System; using System.IO; using FlaxEditor.Scripting; @@ -94,30 +96,8 @@ public class AssetPickerValidator : IContentItemOwner /// public string SelectedPath { - get - { - string path = _selectedItem?.Path ?? _selected?.Path; - if (path != null) - { - // Convert into path relative to the project (cross-platform) - var projectFolder = Globals.ProjectFolder; - if (path.StartsWith(projectFolder)) - path = path.Substring(projectFolder.Length + 1); - } - return path; - } - set - { - if (string.IsNullOrEmpty(value)) - { - SelectedItem = null; - } - else - { - var path = StringUtils.IsRelative(value) ? Path.Combine(Globals.ProjectFolder, value) : value; - SelectedItem = Editor.Instance.ContentDatabase.Find(path); - } - } + get => Utilities.Utils.ToPathProject(_selectedItem?.Path ?? _selected?.Path); + set => SelectedItem = string.IsNullOrEmpty(value) ? null : Editor.Instance.ContentDatabase.Find(Utilities.Utils.ToPathAbsolute(value)); } /// @@ -242,7 +222,7 @@ public class AssetPickerValidator : IContentItemOwner /// /// Initializes a new instance of the class. /// - /// The assets types that this picker accepts. + /// The asset types that this picker accepts. public AssetPickerValidator(ScriptType assetType) { _type = assetType; diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs index 26e1b9842..76f0e12cd 100644 --- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs @@ -6,6 +6,7 @@ using FlaxEditor.Content; using FlaxEditor.GUI; using FlaxEditor.Scripting; using FlaxEngine; +using FlaxEngine.GUI; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors.Editors @@ -50,7 +51,6 @@ namespace FlaxEditor.CustomEditors.Editors if (HasDifferentTypes) return; Picker = layout.Custom().CustomControl; - var value = Values[0]; _valueType = Values.Type.Type != typeof(object) || value == null ? Values.Type : TypeUtils.GetObjectType(value); var assetType = _valueType; @@ -58,37 +58,8 @@ namespace FlaxEditor.CustomEditors.Editors assetType = new ScriptType(typeof(Asset)); else if (_valueType.Type != null && _valueType.Type.Name == typeof(JsonAssetReference<>).Name) assetType = new ScriptType(_valueType.Type.GenericTypeArguments[0]); - - float height = 48; - var attributes = Values.GetAttributes(); - var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); - if (assetReference != null) - { - if (assetReference.UseSmallPicker) - height = 32; - - if (string.IsNullOrEmpty(assetReference.TypeName)) - { - } - else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') - { - // Generic file picker - assetType = ScriptType.Null; - Picker.Validator.FileExtension = assetReference.TypeName; - } - else - { - var customType = TypeUtils.GetType(assetReference.TypeName); - if (customType != ScriptType.Null) - assetType = customType; - else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) - Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for asset picker filter.", assetReference.TypeName)); - else - assetType = ScriptType.Void; - } - } - Picker.Validator.AssetType = assetType; + ApplyAssetReferenceAttribute(Values, out var height, Picker.Validator); Picker.Height = height; Picker.SelectedItemChanged += OnSelectedItemChanged; } @@ -115,6 +86,37 @@ namespace FlaxEditor.CustomEditors.Editors SetValue(Picker.Validator.SelectedAsset); } + internal static void ApplyAssetReferenceAttribute(ValueContainer values, out float height, AssetPickerValidator validator) + { + height = 48; + var attributes = values.GetAttributes(); + var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); + if (assetReference != null) + { + if (assetReference.UseSmallPicker) + height = 32; + if (string.IsNullOrEmpty(assetReference.TypeName)) + { + } + else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') + { + // Generic file picker + validator.AssetType = ScriptType.Null; + validator.FileExtension = assetReference.TypeName; + } + else + { + var customType = TypeUtils.GetType(assetReference.TypeName); + if (customType != ScriptType.Null) + validator.AssetType = customType; + else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) + Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for asset picker filter.", assetReference.TypeName)); + else + validator.AssetType = ScriptType.Void; + } + } + } + /// public override void Refresh() { @@ -140,4 +142,155 @@ namespace FlaxEditor.CustomEditors.Editors } } } + + /// + /// Default implementation of the inspector used to edit reference to the files via path (absolute or relative to the project). + /// + /// Supports editing reference to the asset via path using various containers: or or . + public class FilePathEditor : CustomEditor + { + private sealed class TextBoxWithPicker : TextBox + { + private const float DropdownIconMargin = 3.0f; + private const float DropdownIconSize = 12.0f; + private Rectangle DropdownRect => new Rectangle(Width - DropdownIconSize - DropdownIconMargin, DropdownIconMargin, DropdownIconSize, DropdownIconSize); + + public Action ShowPicker; + + public override void Draw() + { + base.Draw(); + + var style = FlaxEngine.GUI.Style.Current; + var dropdownRect = DropdownRect; + Render2D.DrawSprite(style.ArrowDown, dropdownRect, Enabled ? (DropdownRect.Contains(PointFromWindow(RootWindow.MousePosition)) ? style.BorderSelected : style.Foreground) : style.ForegroundDisabled); + } + + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (DropdownRect.Contains(ref location)) + { + Focus(); + ShowPicker(); + return true; + } + + return base.OnMouseDown(location, button); + } + + public override void OnMouseMove(Float2 location) + { + base.OnMouseMove(location); + + if (DropdownRect.Contains(ref location)) + Cursor = CursorType.Default; + else + Cursor = CursorType.IBeam; + } + + protected override Rectangle TextRectangle + { + get + { + var result = base.TextRectangle; + result.Size.X -= DropdownIconSize + DropdownIconMargin * 2; + return result; + } + } + + protected override Rectangle TextClipRectangle + { + get + { + var result = base.TextClipRectangle; + result.Size.X -= DropdownIconSize + DropdownIconMargin * 2; + return result; + } + } + } + + private TextBoxWithPicker _textBox; + private AssetPickerValidator _validator; + private bool _isRefreshing; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + if (HasDifferentTypes) + return; + _textBox = layout.Custom().CustomControl; + _textBox.ShowPicker = OnShowPicker; + _textBox.EditEnd += OnEditEnd; + _validator = new AssetPickerValidator(ScriptType.Null); + AssetRefEditor.ApplyAssetReferenceAttribute(Values, out _, _validator); + } + + private void OnShowPicker() + { + if (_validator.AssetType != ScriptType.Null) + AssetSearchPopup.Show(_textBox, _textBox.BottomLeft, _validator.IsValid, SetPickerPath); + else + ContentSearchPopup.Show(_textBox, _textBox.BottomLeft, _validator.IsValid, SetPickerPath); + } + + private void SetPickerPath(ContentItem item) + { + var path = Utilities.Utils.ToPathProject(item.Path); + SetPath(path); + + _isRefreshing = true; + _textBox.Defocus(); + _textBox.Text = path; + _isRefreshing = false; + + _textBox.RootWindow.Focus(); + _textBox.Focus(); + } + + private void OnEditEnd() + { + SetPath(_textBox.Text); + } + + private string GetPath() + { + var value = Values[0]; + if (value is AssetItem assetItem) + return Utilities.Utils.ToPathProject(assetItem.Path); + if (value is Asset asset) + return Utilities.Utils.ToPathProject(asset.Path); + if (value is string str) + return str; + return null; + } + + private void SetPath(string path) + { + if (_isRefreshing) + return; + var value = Values[0]; + if (value is AssetItem) + SetValue(Editor.Instance.ContentDatabase.Find(Utilities.Utils.ToPathAbsolute(path))); + else if (value is Asset) + SetValue(FlaxEngine.Content.LoadAsync(path)); + else if (value is string) + SetValue(path); + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (!HasDifferentValues) + { + _isRefreshing = true; + _textBox.Text = GetPath(); + _isRefreshing = false; + } + } + } } diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 5c8f02a06..b630836ab 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -5,10 +5,8 @@ using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.Scripting; -using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; -using FlaxEngine.Utilities; namespace FlaxEditor.GUI { diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index fbbbd71bf..b14d58eb6 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -1471,5 +1471,27 @@ namespace FlaxEditor.Utilities inputActions.Add(options => options.GenerateScriptsProject, () => Editor.Instance.ProgressReporting.GenerateScriptsProjectFiles.RunAsync()); inputActions.Add(options => options.RecompileScripts, ScriptsBuilder.Compile); } + + internal static string ToPathProject(string path) + { + if (path != null) + { + // Convert into path relative to the project (cross-platform) + var projectFolder = Globals.ProjectFolder; + if (path.StartsWith(projectFolder)) + path = path.Substring(projectFolder.Length + 1); + } + return path; + } + + internal static string ToPathAbsolute(string path) + { + if (path != null) + { + // Convert into global path to if relative to the project + path = StringUtils.IsRelative(path) ? Path.Combine(Globals.ProjectFolder, path) : path; + } + return path; + } } } diff --git a/Source/Engine/Video/VideoPlayer.h b/Source/Engine/Video/VideoPlayer.h index 34507626b..dd5fccd28 100644 --- a/Source/Engine/Video/VideoPlayer.h +++ b/Source/Engine/Video/VideoPlayer.h @@ -49,7 +49,7 @@ public: /// /// The video clip Url path used as a source of the media. Can be local file (absolute or relative path), or streamed resource ('http://'). /// - API_FIELD(Attributes="EditorOrder(10), DefaultValue(\"\"), EditorDisplay(\"Video Player\")") + API_FIELD(Attributes="EditorOrder(10), DefaultValue(\"\"), EditorDisplay(\"Video Player\"), AssetReference(\".mp4\"), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.FilePathEditor\")") String Url; ///