diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 2a150306d..663acf05f 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -2,35 +2,35 @@ using System.Collections.Generic; using FlaxEngine; -namespace %namespace% +namespace %namespace%; + +/// +/// %class% Script. +/// +public class %class% : Script { - /// - /// %class% Script. - /// - public class %class% : Script + /// + public override void OnStart() { - /// - public override void OnStart() - { - // Here you can add code that needs to be called when script is created, just before the first game update - } - - /// - public override void OnEnable() - { - // Here you can add code that needs to be called when script is enabled (eg. register for events) - } + // Here you can add code that needs to be called when script is created, just before the first game update + } + + /// + public override void OnEnable() + { + // Here you can add code that needs to be called when script is enabled (eg. register for events) + } - /// - public override void OnDisable() - { - // Here you can add code that needs to be called when script is disabled (eg. unregister from events) - } + /// + public override void OnDisable() + { + // Here you can add code that needs to be called when script is disabled (eg. unregister from events) + } - /// - public override void OnUpdate() - { - // Here you can add code that needs to be called every frame - } + /// + public override void OnUpdate() + { + // Here you can add code that needs to be called every frame } } + diff --git a/Flax.flaxproj b/Flax.flaxproj index fbf48cc2b..ebcdc2830 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -2,8 +2,8 @@ "Name": "Flax", "Version": { "Major": 1, - "Minor": 6, - "Build": 6345 + "Minor": 7, + "Build": 6401 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.", diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings index 6a22f211b..ff396d824 100644 --- a/Flax.sln.DotSettings +++ b/Flax.sln.DotSettings @@ -256,6 +256,8 @@ True True True + True + True True True True @@ -321,6 +323,7 @@ True True True + True True True True diff --git a/Source/Editor/Content/GUI/ContentView.DragDrop.cs b/Source/Editor/Content/GUI/ContentView.DragDrop.cs index 348e2b443..ffce81e2d 100644 --- a/Source/Editor/Content/GUI/ContentView.DragDrop.cs +++ b/Source/Editor/Content/GUI/ContentView.DragDrop.cs @@ -68,7 +68,7 @@ namespace FlaxEditor.Content.GUI _validDragOver = true; result = DragDropEffect.Copy; } - else if (_dragActors.HasValidDrag) + else if (_dragActors != null && _dragActors.HasValidDrag) { _validDragOver = true; result = DragDropEffect.Move; @@ -94,7 +94,7 @@ namespace FlaxEditor.Content.GUI result = DragDropEffect.Copy; } // Check if drop actor(s) - else if (_dragActors.HasValidDrag) + else if (_dragActors != null && _dragActors.HasValidDrag) { // Import actors var currentFolder = Editor.Instance.Windows.ContentWin.CurrentViewFolder; diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs index 5e054e5c6..e1a3acdf6 100644 --- a/Source/Editor/Content/GUI/ContentView.cs +++ b/Source/Editor/Content/GUI/ContentView.cs @@ -520,8 +520,8 @@ namespace FlaxEditor.Content.GUI { int min = _selection.Min(x => x.IndexInParent); int max = _selection.Max(x => x.IndexInParent); - min = Mathf.Min(min, item.IndexInParent); - max = Mathf.Max(max, item.IndexInParent); + min = Mathf.Max(Mathf.Min(min, item.IndexInParent), 0); + max = Mathf.Min(Mathf.Max(max, item.IndexInParent), _children.Count - 1); var selection = new List(_selection); for (int i = min; i <= max; i++) { diff --git a/Source/Editor/Content/Import/AudioImportSettings.cs b/Source/Editor/Content/Import/AudioImportSettings.cs index 2fc0a1795..e0bfb6e20 100644 --- a/Source/Editor/Content/Import/AudioImportSettings.cs +++ b/Source/Editor/Content/Import/AudioImportSettings.cs @@ -1,144 +1,52 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. -using System.ComponentModel; -using System.Reflection; -using System.Runtime.InteropServices; +using System.Collections.Generic; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.Scripting; using FlaxEngine; -using FlaxEngine.Interop; +using FlaxEngine.Tools; + +namespace FlaxEngine.Tools +{ + partial class AudioTool + { + partial struct Options + { + private bool ShowBtiDepth => Format != AudioFormat.Vorbis; + } + } +} + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + [CustomEditor(typeof(FlaxEngine.Tools.AudioTool.Options)), DefaultEditor] + public class AudioToolOptionsEditor : GenericEditor + { + /// + protected override List GetItemsForType(ScriptType type) + { + // Show both fields and properties + return GetItemsForType(type, true, true); + } + } +} namespace FlaxEditor.Content.Import { /// /// Proxy object to present audio import settings in . /// + [HideInEditor] public class AudioImportSettings { /// - /// A custom set of bit depth audio import sizes. + /// The settings data. /// - public enum CustomBitDepth - { - /// - /// The 8. - /// - _8 = 8, - - /// - /// The 16. - /// - _16 = 16, - - /// - /// The 24. - /// - _24 = 24, - - /// - /// The 32. - /// - _32 = 32, - } - - /// - /// Converts the bit depth to enum. - /// - /// The bit depth. - /// The converted enum. - public static CustomBitDepth ConvertBitDepth(int f) - { - FieldInfo[] fields = typeof(CustomBitDepth).GetFields(); - for (int i = 0; i < fields.Length; i++) - { - var field = fields[i]; - if (field.Name.Equals("value__")) - continue; - - if (f == (int)field.GetRawConstantValue()) - return (CustomBitDepth)f; - } - - return CustomBitDepth._16; - } - - /// - /// The audio data format to import the audio clip as. - /// - [EditorOrder(10), DefaultValue(AudioFormat.Vorbis), Tooltip("The audio data format to import the audio clip as.")] - public AudioFormat Format { get; set; } = AudioFormat.Vorbis; - - /// - /// The audio data compression quality. Used only if target format is using compression. Value 0 means the smallest size, value 1 means the best quality. - /// - [EditorOrder(15), DefaultValue(0.4f), Limit(0, 1, 0.01f), Tooltip("The audio data compression quality. Used only if target format is using compression. Value 0 means the smallest size, value 1 means the best quality.")] - public float CompressionQuality { get; set; } = 0.4f; - - /// - /// Disables dynamic audio streaming. The whole clip will be loaded into the memory. Useful for small clips (eg. gunfire sounds). - /// - [EditorOrder(20), DefaultValue(false), Tooltip("Disables dynamic audio streaming. The whole clip will be loaded into the memory. Useful for small clips (eg. gunfire sounds).")] - public bool DisableStreaming { get; set; } = false; - - /// - /// Checks should the clip be played as spatial (3D) audio or as normal audio. 3D audio is stored in Mono format. - /// - [EditorOrder(30), DefaultValue(false), EditorDisplay(null, "Is 3D"), Tooltip("Checks should the clip be played as spatial (3D) audio or as normal audio. 3D audio is stored in Mono format.")] - public bool Is3D { get; set; } = false; - - /// - /// The size of a single sample in bits. The clip will be converted to this bit depth on import. - /// - [EditorOrder(40), DefaultValue(CustomBitDepth._16), Tooltip("The size of a single sample in bits. The clip will be converted to this bit depth on import.")] - public CustomBitDepth BitDepth { get; set; } = CustomBitDepth._16; - - [StructLayout(LayoutKind.Sequential)] - internal struct InternalOptions - { - [MarshalAs(UnmanagedType.I1)] - public AudioFormat Format; - public byte DisableStreaming; - public byte Is3D; - public int BitDepth; - public float Quality; - } - - internal void ToInternal(out InternalOptions options) - { - options = new InternalOptions - { - Format = Format, - DisableStreaming = (byte)(DisableStreaming ? 1 : 0), - Is3D = (byte)(Is3D ? 1 : 0), - Quality = CompressionQuality, - BitDepth = (int)BitDepth, - }; - } - - internal void FromInternal(ref InternalOptions options) - { - Format = options.Format; - DisableStreaming = options.DisableStreaming != 0; - Is3D = options.Is3D != 0; - CompressionQuality = options.Quality; - BitDepth = ConvertBitDepth(options.BitDepth); - } - - /// - /// Tries the restore the asset import options from the target resource file. - /// - /// The options. - /// The asset path. - /// True settings has been restored, otherwise false. - public static bool TryRestore(ref AudioImportSettings options, string assetPath) - { - if (AudioImportEntry.Internal_GetAudioImportOptions(assetPath, out var internalOptions)) - { - // Restore settings - options.FromInternal(ref internalOptions); - return true; - } - - return false; - } + [EditorDisplay(null, EditorDisplayAttribute.InlineStyle)] + public AudioTool.Options Settings = AudioTool.Options.Default; } /// @@ -147,7 +55,7 @@ namespace FlaxEditor.Content.Import /// public partial class AudioImportEntry : AssetImportEntry { - private AudioImportSettings _settings = new AudioImportSettings(); + private AudioImportSettings _settings = new(); /// /// Initializes a new instance of the class. @@ -157,7 +65,7 @@ namespace FlaxEditor.Content.Import : base(ref request) { // Try to restore target asset Audio import options (useful for fast reimport) - AudioImportSettings.TryRestore(ref _settings, ResultUrl); + Editor.TryRestoreImportOptions(ref _settings.Settings, ResultUrl); } /// @@ -166,27 +74,23 @@ namespace FlaxEditor.Content.Import /// public override bool TryOverrideSettings(object settings) { - if (settings is AudioImportSettings o) + if (settings is AudioImportSettings s) { - _settings = o; + _settings.Settings = s.Settings; + return true; + } + if (settings is AudioTool.Options o) + { + _settings.Settings = o; return true; } - return false; } /// public override bool Import() { - return Editor.Import(SourceUrl, ResultUrl, _settings); + return Editor.Import(SourceUrl, ResultUrl, _settings.Settings); } - - #region Internal Calls - - [LibraryImport("FlaxEngine", EntryPoint = "AudioImportEntryInternal_GetAudioImportOptions", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_GetAudioImportOptions(string path, out AudioImportSettings.InternalOptions result); - - #endregion } } diff --git a/Source/Editor/Content/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 94db3a5b9..6bbdc0a51 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -486,7 +486,7 @@ namespace FlaxEditor.Content else Render2D.FillRectangle(rectangle, Color.Black); } - + /// /// Draws the item thumbnail. /// @@ -684,7 +684,7 @@ namespace FlaxEditor.Content var thumbnailSize = size.X; thumbnailRect = new Rectangle(0, 0, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Center; - + if (this is ContentFolder) { // Small shadow @@ -692,7 +692,7 @@ namespace FlaxEditor.Content var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); - + if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) @@ -706,14 +706,14 @@ namespace FlaxEditor.Content var shadowRect = new Rectangle(2, 2, clientRect.Width + 1, clientRect.Height + 1); var color = Color.Black.AlphaMultiplied(0.2f); Render2D.FillRectangle(shadowRect, color); - + Render2D.FillRectangle(clientRect, style.Background.RGBMultiplied(1.25f)); Render2D.FillRectangle(TextRectangle, style.LightBackground); - + var accentHeight = 2 * view.ViewScale; var barRect = new Rectangle(0, thumbnailRect.Height - accentHeight, clientRect.Width, accentHeight); Render2D.FillRectangle(barRect, Color.DimGray); - + DrawThumbnail(ref thumbnailRect, false); if (isSelected) { @@ -733,18 +733,18 @@ namespace FlaxEditor.Content var thumbnailSize = size.Y - 2 * DefaultMarginSize; thumbnailRect = new Rectangle(DefaultMarginSize, DefaultMarginSize, thumbnailSize, thumbnailSize); nameAlignment = TextAlignment.Near; - + if (isSelected) Render2D.FillRectangle(clientRect, Parent.ContainsFocus ? style.BackgroundSelected : style.LightBackground); else if (IsMouseOver) Render2D.FillRectangle(clientRect, style.BackgroundHighlighted); - + DrawThumbnail(ref thumbnailRect); break; } default: throw new ArgumentOutOfRangeException(); } - + // Draw short name Render2D.PushClip(ref textRect); Render2D.DrawText(style.FontMedium, ShowFileExtension || view.ShowFileExtensions ? FileName : ShortName, textRect, style.Foreground, nameAlignment, TextAlignment.Center, TextWrapping.WrapWords, 1f, 0.95f); diff --git a/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs b/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs new file mode 100644 index 000000000..33ad0862f --- /dev/null +++ b/Source/Editor/Content/Proxy/BehaviorTreeProxy.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using FlaxEditor.Content.Thumbnails; +using FlaxEditor.Windows; +using FlaxEditor.Windows.Assets; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content +{ + /// + /// A asset proxy object. + /// + /// + [ContentContextMenu("New/AI/Behavior Tree")] + public class BehaviorTreeProxy : BinaryAssetProxy + { + /// + public override string Name => "Behavior Tree"; + + /// + public override bool CanReimport(ContentItem item) + { + return true; + } + + /// + public override EditorWindow Open(Editor editor, ContentItem item) + { + return new BehaviorTreeWindow(editor, item as BinaryAssetItem); + } + + /// + public override Color AccentColor => Color.FromRGB(0x3256A8); + + /// + public override Type AssetType => typeof(BehaviorTree); + + /// + public override bool CanCreate(ContentFolder targetLocation) + { + return targetLocation.CanHaveAssets; + } + + /// + public override void Create(string outputPath, object arg) + { + if (Editor.CreateAsset(Editor.NewAssetType.BehaviorTree, outputPath)) + throw new Exception("Failed to create new asset."); + } + + /// + public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) + { + guiRoot.AddChild(new Label + { + Text = Path.GetFileNameWithoutExtension(request.Asset.Path), + Offsets = Margin.Zero, + AnchorPreset = AnchorPresets.StretchAll, + Wrapping = TextWrapping.WrapWords + }); + } + } +} diff --git a/Source/Editor/Content/Proxy/CubeTextureProxy.cs b/Source/Editor/Content/Proxy/CubeTextureProxy.cs index 691e4ac53..2dccc9e47 100644 --- a/Source/Editor/Content/Proxy/CubeTextureProxy.cs +++ b/Source/Editor/Content/Proxy/CubeTextureProxy.cs @@ -54,12 +54,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if all mip maps are streamed - var asset = (CubeTexture)request.Asset; - return asset.ResidentMipLevels >= Mathf.Max(1, (int)(asset.MipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((CubeTexture)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index dcc3e3ec4..331ff81c3 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -62,7 +62,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - return _preview.HasLoadedAssets; + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((MaterialInstance)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index cf7c8b77e..a7fcfecc8 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -106,7 +106,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - return _preview.HasLoadedAssets; + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Material)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 845cbc80b..0cf16850d 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -82,12 +82,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if asset is streamed enough - var asset = (Model)request.Asset; - return asset.LoadedLODs >= Mathf.Max(1, (int)(asset.LODs.Length * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Model)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs index f0193d0d0..6e228aa86 100644 --- a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs +++ b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs @@ -54,15 +54,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - if (!_preview.HasLoadedAssets) - return false; - - // Check if asset is streamed enough - var asset = (SkinnedModel)request.Asset; - var lods = asset.LODs.Length; - if (asset.IsLoaded && lods == 0) - return true; // Skeleton-only model - return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((SkinnedModel)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/TextureProxy.cs b/Source/Editor/Content/Proxy/TextureProxy.cs index 48bfcc9e9..709ca4dbf 100644 --- a/Source/Editor/Content/Proxy/TextureProxy.cs +++ b/Source/Editor/Content/Proxy/TextureProxy.cs @@ -57,11 +57,7 @@ namespace FlaxEditor.Content /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - // Check if asset is streamed enough - var asset = (Texture)request.Asset; - var mipLevels = asset.MipLevels; - var minMipLevels = Mathf.Min(mipLevels, 7); - return asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return ThumbnailsModule.HasMinimumQuality((Texture)request.Asset); } /// diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index a6996310e..160c783ed 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -125,6 +125,74 @@ namespace FlaxEditor.Content.Thumbnails } } + internal static bool HasMinimumQuality(TextureBase asset) + { + var mipLevels = asset.MipLevels; + var minMipLevels = Mathf.Min(mipLevels, 7); + return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(Model asset) + { + if (!asset.IsLoaded) + return false; + var lods = asset.LODs.Length; + var slots = asset.MaterialSlots; + foreach (var slot in slots) + { + if (slot.Material && !HasMinimumQuality(slot.Material)) + return false; + } + return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(SkinnedModel asset) + { + var lods = asset.LODs.Length; + if (asset.IsLoaded && lods == 0) + return true; // Skeleton-only model + var slots = asset.MaterialSlots; + foreach (var slot in slots) + { + if (slot.Material && !HasMinimumQuality(slot.Material)) + return false; + } + return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); + } + + internal static bool HasMinimumQuality(MaterialBase asset) + { + if (asset is MaterialInstance asInstance) + return HasMinimumQuality(asInstance); + return HasMinimumQualityInternal(asset); + } + + internal static bool HasMinimumQuality(Material asset) + { + return HasMinimumQualityInternal(asset); + } + + internal static bool HasMinimumQuality(MaterialInstance asset) + { + if (!HasMinimumQualityInternal(asset)) + return false; + var baseMaterial = asset.BaseMaterial; + return baseMaterial == null || HasMinimumQualityInternal(baseMaterial); + } + + private static bool HasMinimumQualityInternal(MaterialBase asset) + { + if (!asset.IsLoaded) + return false; + var parameters = asset.Parameters; + foreach (var parameter in parameters) + { + if (parameter.Value is TextureBase asTexture && !HasMinimumQuality(asTexture)) + return false; + } + return true; + } + #region IContentItemOwner /// @@ -368,7 +436,6 @@ namespace FlaxEditor.Content.Thumbnails // Create atlas if (PreviewsCache.Create(path)) { - // Error Editor.LogError("Failed to create thumbnails atlas."); return null; } @@ -377,7 +444,6 @@ namespace FlaxEditor.Content.Thumbnails var atlas = FlaxEngine.Content.LoadAsync(path); if (atlas == null) { - // Error Editor.LogError("Failed to load thumbnails atlas."); return null; } diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index b66a96a9b..a1db61dbb 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -17,7 +17,7 @@ #include "Editor/ProjectInfo.h" #include "Editor/Cooker/GameCooker.h" #include "Editor/Utilities/EditorUtilities.h" -#include +#include using namespace pugi; IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform); @@ -170,17 +170,17 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) const String plistPath = data.DataOutputPath / TEXT("Info.plist"); { xml_document doc; - xml_node plist = doc.child_or_append(PUGIXML_TEXT("plist")); + xml_node_extra plist = xml_node_extra(doc).child_or_append(PUGIXML_TEXT("plist")); plist.append_attribute(PUGIXML_TEXT("version")).set_value(PUGIXML_TEXT("1.0")); - xml_node dict = plist.child_or_append(PUGIXML_TEXT("dict")); + xml_node_extra dict = plist.child_or_append(PUGIXML_TEXT("dict")); #define ADD_ENTRY(key, value) \ - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ - dict.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT(value)) + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \ + dict.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT(value)) #define ADD_ENTRY_STR(key, value) \ - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT(key)); \ + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT(key)); \ { std::u16string valueStr(value.GetText()); \ - dict.append_child(PUGIXML_TEXT("string")).set_child_value(pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); } + dict.append_child_with_value(PUGIXML_TEXT("string"), pugi::string_t(valueStr.begin(), valueStr.end()).c_str()); } ADD_ENTRY("CFBundleDevelopmentRegion", "English"); ADD_ENTRY("CFBundlePackageType", "APPL"); @@ -194,22 +194,22 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) ADD_ENTRY_STR("CFBundleVersion", projectVersion); ADD_ENTRY_STR("NSHumanReadableCopyright", gameSettings->CopyrightNotice); - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("CFBundleSupportedPlatforms")); - xml_node CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array")); - CFBundleSupportedPlatforms.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("MacOSX")); + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("CFBundleSupportedPlatforms")); + xml_node_extra CFBundleSupportedPlatforms = dict.append_child(PUGIXML_TEXT("array")); + CFBundleSupportedPlatforms.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("MacOSX")); - dict.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture")); - xml_node LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict")); + dict.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("LSMinimumSystemVersionByArchitecture")); + xml_node_extra LSMinimumSystemVersionByArchitecture = dict.append_child(PUGIXML_TEXT("dict")); switch (_arch) { case ArchitectureType::x64: - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("x86_64")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("x86_64")); break; case ArchitectureType::ARM64: - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("key")).set_child_value(PUGIXML_TEXT("arm64")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("key"), PUGIXML_TEXT("arm64")); break; } - LSMinimumSystemVersionByArchitecture.append_child(PUGIXML_TEXT("string")).set_child_value(PUGIXML_TEXT("10.15")); + LSMinimumSystemVersionByArchitecture.append_child_with_value(PUGIXML_TEXT("string"), PUGIXML_TEXT("10.15")); #undef ADD_ENTRY #undef ADD_ENTRY_STR diff --git a/Source/Editor/CustomEditors/CustomEditorPresenter.cs b/Source/Editor/CustomEditors/CustomEditorPresenter.cs index 73e2ede0f..0c4ac4106 100644 --- a/Source/Editor/CustomEditors/CustomEditorPresenter.cs +++ b/Source/Editor/CustomEditors/CustomEditorPresenter.cs @@ -37,6 +37,23 @@ namespace FlaxEditor.CustomEditors UseDefault = 1 << 2, } + /// + /// The interface for Editor context that owns the presenter. Can be or or other window/panel - custom editor scan use it for more specific features. + /// + public interface IPresenterOwner + { + /// + /// Gets the viewport linked with properties presenter (optional, null if unused). + /// + public Viewport.EditorViewport PresenterViewport { get; } + + /// + /// Selects the scene objects. + /// + /// The nodes to select + public void Select(List nodes); + } + /// /// Main class for Custom Editors used to present selected objects properties and allow to modify them. /// @@ -68,8 +85,15 @@ namespace FlaxEditor.CustomEditors /// public override void Update(float deltaTime) { - // Update editors - _presenter.Update(); + try + { + // Update editors + _presenter.Update(); + } + catch (Exception ex) + { + FlaxEditor.Editor.LogWarning(ex); + } base.Update(deltaTime); } @@ -254,7 +278,7 @@ namespace FlaxEditor.CustomEditors /// /// The Editor context that owns this presenter. Can be or or other window/panel - custom editor scan use it for more specific features. /// - public object Owner; + public IPresenterOwner Owner; /// /// Gets or sets the text to show when no object is selected. @@ -270,7 +294,24 @@ namespace FlaxEditor.CustomEditors } } + /// + /// Gets or sets the value indicating whether properties are read-only. + /// + public bool ReadOnly + { + get => _readOnly; + set + { + if (_readOnly != value) + { + _readOnly = value; + UpdateReadOnly(); + } + } + } + private bool _buildOnUpdate; + private bool _readOnly; /// /// Initializes a new instance of the class. @@ -278,7 +319,7 @@ namespace FlaxEditor.CustomEditors /// The undo. It's optional. /// The custom text to display when no object is selected. Default is No selection. /// The owner of the presenter. - public CustomEditorPresenter(Undo undo, string noSelectionText = null, object owner = null) + public CustomEditorPresenter(Undo undo, string noSelectionText = null, IPresenterOwner owner = null) { Undo = undo; Owner = owner; @@ -364,6 +405,8 @@ namespace FlaxEditor.CustomEditors // Restore scroll value if (parentScrollV > -1) panel.VScrollBar.Value = parentScrollV; + if (_readOnly) + UpdateReadOnly(); } /// @@ -374,6 +417,16 @@ namespace FlaxEditor.CustomEditors _buildOnUpdate = true; } + private void UpdateReadOnly() + { + // Only scrollbars are enabled + foreach (var child in Panel.Children) + { + if (!(child is ScrollBar)) + child.Enabled = !_readOnly; + } + } + private void ExpandGroups(LayoutElementsContainer c, bool open) { if (c is Elements.GroupElement group) diff --git a/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs b/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs new file mode 100644 index 000000000..c6043e8b6 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ClothEditor.cs @@ -0,0 +1,97 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.Gizmo; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Json; +using FlaxEngine.Tools; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(Cloth)), DefaultEditor] + class ClothEditor : ActorEditor + { + private ClothPaintingGizmoMode _gizmoMode; + private Viewport.Modes.EditorGizmoMode _prevMode; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (Values.Count != 1) + return; + + // Add gizmo painting mode to the viewport + var owner = Presenter.Owner; + if (owner == null) + return; + var gizmoOwner = owner as IGizmoOwner ?? owner.PresenterViewport as IGizmoOwner; + if (gizmoOwner == null) + return; + var gizmos = gizmoOwner.Gizmos; + _gizmoMode = new ClothPaintingGizmoMode(); + + var projectCache = Editor.Instance.ProjectCache; + if (projectCache.TryGetCustomData("ClothGizmoPaintValue", out var cachedPaintValue)) + _gizmoMode.PaintValue = JsonSerializer.Deserialize(cachedPaintValue); + if (projectCache.TryGetCustomData("ClothGizmoContinuousPaint", out var cachedContinuousPaint)) + _gizmoMode.ContinuousPaint = JsonSerializer.Deserialize(cachedContinuousPaint); + if (projectCache.TryGetCustomData("ClothGizmoBrushFalloff", out var cachedBrushFalloff)) + _gizmoMode.BrushFalloff = JsonSerializer.Deserialize(cachedBrushFalloff); + if (projectCache.TryGetCustomData("ClothGizmoBrushSize", out var cachedBrushSize)) + _gizmoMode.BrushSize = JsonSerializer.Deserialize(cachedBrushSize); + if (projectCache.TryGetCustomData("ClothGizmoBrushStrength", out var cachedBrushStrength)) + _gizmoMode.BrushStrength = JsonSerializer.Deserialize(cachedBrushStrength); + + gizmos.AddMode(_gizmoMode); + _prevMode = gizmos.ActiveMode; + gizmos.ActiveMode = _gizmoMode; + _gizmoMode.Gizmo.SetPaintCloth((Cloth)Values[0]); + + // Insert gizmo mode options to properties editing + var paintGroup = layout.Group("Cloth Painting"); + var paintValue = new ReadOnlyValueContainer(new ScriptType(typeof(ClothPaintingGizmoMode)), _gizmoMode); + paintGroup.Object(paintValue); + { + var grid = paintGroup.CustomContainer(); + var gridControl = grid.CustomControl; + gridControl.ClipChildren = false; + gridControl.Height = Button.DefaultHeight; + gridControl.SlotsHorizontally = 2; + gridControl.SlotsVertically = 1; + grid.Button("Fill", "Fills the cloth particles with given paint value.").Button.Clicked += _gizmoMode.Gizmo.Fill; + grid.Button("Reset", "Clears the cloth particles paint.").Button.Clicked += _gizmoMode.Gizmo.Reset; + } + } + + /// + protected override void Deinitialize() + { + // Cleanup gizmos + if (_gizmoMode != null) + { + var gizmos = _gizmoMode.Owner.Gizmos; + if (gizmos.ActiveMode == _gizmoMode) + gizmos.ActiveMode = _prevMode; + gizmos.RemoveMode(_gizmoMode); + var projectCache = Editor.Instance.ProjectCache; + projectCache.SetCustomData("ClothGizmoPaintValue", JsonSerializer.Serialize(_gizmoMode.PaintValue, typeof(float))); + projectCache.SetCustomData("ClothGizmoContinuousPaint", JsonSerializer.Serialize(_gizmoMode.ContinuousPaint, typeof(bool))); + projectCache.SetCustomData("ClothGizmoBrushFalloff", JsonSerializer.Serialize(_gizmoMode.BrushFalloff, typeof(float))); + projectCache.SetCustomData("ClothGizmoBrushSize", JsonSerializer.Serialize(_gizmoMode.BrushSize, typeof(float))); + projectCache.SetCustomData("ClothGizmoBrushStrength", JsonSerializer.Serialize(_gizmoMode.BrushStrength, typeof(float))); + _gizmoMode.Dispose(); + _gizmoMode = null; + } + _prevMode = null; + + base.Deinitialize(); + } + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs new file mode 100644 index 000000000..71c5f6daf --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -0,0 +1,336 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.CustomEditors.Elements; +using FlaxEditor.GUI; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(ModelInstanceActor.MeshReference)), DefaultEditor] + public class MeshReferenceEditor : CustomEditor + { + private class MeshRefPickerControl : Control + { + private ModelInstanceActor.MeshReference _value = new ModelInstanceActor.MeshReference { LODIndex = -1, MeshIndex = -1 }; + private string _valueName; + private Float2 _mousePos; + + public string[][] MeshNames; + public event Action ValueChanged; + + public ModelInstanceActor.MeshReference Value + { + get => _value; + set + { + if (_value.LODIndex == value.LODIndex && _value.MeshIndex == value.MeshIndex) + return; + _value = value; + if (value.LODIndex == -1 || value.MeshIndex == -1) + _valueName = null; + else if (MeshNames.Length == 1) + _valueName = MeshNames[value.LODIndex][value.MeshIndex]; + else + _valueName = $"LOD{value.LODIndex} - {MeshNames[value.LODIndex][value.MeshIndex]}"; + ValueChanged?.Invoke(); + } + } + + public MeshRefPickerControl() + : base(0, 0, 50, 16) + { + } + + private void ShowDropDownMenu() + { + // Show context menu with tree structure of LODs and meshes + Focus(); + var cm = new ItemsListContextMenu(200); + var meshNames = MeshNames; + var actor = _value.Actor; + for (int lodIndex = 0; lodIndex < meshNames.Length; lodIndex++) + { + var item = new ItemsListContextMenu.Item + { + Name = "LOD" + lodIndex, + Tag = new ModelInstanceActor.MeshReference { Actor = actor, LODIndex = lodIndex, MeshIndex = 0 }, + TintColor = new Color(0.8f, 0.8f, 1.0f, 0.8f), + }; + cm.AddItem(item); + + for (int meshIndex = 0; meshIndex < meshNames[lodIndex].Length; meshIndex++) + { + item = new ItemsListContextMenu.Item + { + Name = " " + meshNames[lodIndex][meshIndex], + Tag = new ModelInstanceActor.MeshReference { Actor = actor, LODIndex = lodIndex, MeshIndex = meshIndex }, + }; + if (_value.LODIndex == lodIndex && _value.MeshIndex == meshIndex) + item.BackgroundColor = FlaxEngine.GUI.Style.Current.BackgroundSelected; + cm.AddItem(item); + } + } + cm.ItemClicked += item => Value = (ModelInstanceActor.MeshReference)item.Tag; + cm.Show(Parent, BottomLeft); + } + + /// + public override void Draw() + { + base.Draw(); + + // Cache data + var style = FlaxEngine.GUI.Style.Current; + bool isSelected = _valueName != null; + bool isEnabled = EnabledInHierarchy; + var frameRect = new Rectangle(0, 0, Width, 16); + if (isSelected) + frameRect.Width -= 16; + frameRect.Width -= 16; + var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14); + var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14); + var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14); + + // Draw frame + Render2D.DrawRectangle(frameRect, isEnabled && (IsMouseOver || IsNavFocused) ? style.BorderHighlighted : style.BorderNormal); + + // Check if has item selected + if (isSelected) + { + // Draw name + Render2D.PushClip(nameRect); + Render2D.DrawText(style.FontMedium, _valueName, nameRect, isEnabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center); + Render2D.PopClip(); + + // Draw deselect button + Render2D.DrawSprite(style.Cross, button1Rect, isEnabled && button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); + } + else + { + // Draw info + Render2D.DrawText(style.FontMedium, "-", nameRect, isEnabled ? Color.OrangeRed : Color.DarkOrange, TextAlignment.Near, TextAlignment.Center); + } + + // Draw picker button + var pickerRect = isSelected ? button2Rect : button1Rect; + Render2D.DrawSprite(style.ArrowDown, pickerRect, isEnabled && pickerRect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); + } + + /// + public override void OnMouseEnter(Float2 location) + { + _mousePos = location; + + base.OnMouseEnter(location); + } + + /// + public override void OnMouseLeave() + { + _mousePos = Float2.Minimum; + + base.OnMouseLeave(); + } + + /// + public override void OnMouseMove(Float2 location) + { + _mousePos = location; + + base.OnMouseMove(location); + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + // Cache data + bool isSelected = _valueName != null; + var frameRect = new Rectangle(0, 0, Width, 16); + if (isSelected) + frameRect.Width -= 16; + frameRect.Width -= 16; + var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14); + var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14); + var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14); + + // Deselect + if (isSelected && button1Rect.Contains(ref location)) + Value = new ModelInstanceActor.MeshReference { Actor = null, LODIndex = -1, MeshIndex = -1 }; + + // Picker dropdown menu + if ((isSelected ? button2Rect : button1Rect).Contains(ref location)) + ShowDropDownMenu(); + + return base.OnMouseUp(location, button); + } + + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + Focus(); + + // Open model editor window + if (_value.Actor is StaticModel staticModel) + Editor.Instance.ContentEditing.Open(staticModel.Model); + else if (_value.Actor is AnimatedModel animatedModel) + Editor.Instance.ContentEditing.Open(animatedModel.SkinnedModel); + + return base.OnMouseDoubleClick(location, button); + } + + /// + public override void OnSubmit() + { + base.OnSubmit(); + + ShowDropDownMenu(); + } + + /// + public override void OnDestroy() + { + MeshNames = null; + _valueName = null; + + base.OnDestroy(); + } + } + + private ModelInstanceActor _actor; + private CustomElement _actorPicker; + private CustomElement _meshPicker; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + // Get the context actor to pick the mesh from it + if (GetActor(out var actor)) + { + // TODO: support editing multiple values + layout.Label("Different values"); + return; + } + _actor = actor; + + var showActorPicker = actor == null || ParentEditor.Values.All(x => x is not Cloth); + if (showActorPicker) + { + // Actor reference picker + _actorPicker = layout.Custom(); + _actorPicker.CustomControl.Type = new ScriptType(typeof(ModelInstanceActor)); + _actorPicker.CustomControl.ValueChanged += () => SetValue(new ModelInstanceActor.MeshReference { Actor = (ModelInstanceActor)_actorPicker.CustomControl.Value }); + } + + if (actor != null) + { + // Get mesh names hierarchy + string[][] meshNames; + if (actor is StaticModel staticModel) + { + var model = staticModel.Model; + if (model == null || model.WaitForLoaded()) + return; + var materials = model.MaterialSlots; + var lods = model.LODs; + meshNames = new string[lods.Length][]; + for (int lodIndex = 0; lodIndex < lods.Length; lodIndex++) + { + var lodMeshes = lods[lodIndex].Meshes; + meshNames[lodIndex] = new string[lodMeshes.Length]; + for (int meshIndex = 0; meshIndex < lodMeshes.Length; meshIndex++) + { + var mesh = lodMeshes[meshIndex]; + var materialName = materials[mesh.MaterialSlotIndex].Name; + if (string.IsNullOrEmpty(materialName) && materials[mesh.MaterialSlotIndex].Material) + materialName = Path.GetFileNameWithoutExtension(materials[mesh.MaterialSlotIndex].Material.Path); + if (string.IsNullOrEmpty(materialName)) + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex}"; + else + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex} ({materialName})"; + } + } + } + else if (actor is AnimatedModel animatedModel) + { + var skinnedModel = animatedModel.SkinnedModel; + if (skinnedModel == null || skinnedModel.WaitForLoaded()) + return; + var materials = skinnedModel.MaterialSlots; + var lods = skinnedModel.LODs; + meshNames = new string[lods.Length][]; + for (int lodIndex = 0; lodIndex < lods.Length; lodIndex++) + { + var lodMeshes = lods[lodIndex].Meshes; + meshNames[lodIndex] = new string[lodMeshes.Length]; + for (int meshIndex = 0; meshIndex < lodMeshes.Length; meshIndex++) + { + var mesh = lodMeshes[meshIndex]; + var materialName = materials[mesh.MaterialSlotIndex].Name; + if (string.IsNullOrEmpty(materialName) && materials[mesh.MaterialSlotIndex].Material) + materialName = Path.GetFileNameWithoutExtension(materials[mesh.MaterialSlotIndex].Material.Path); + if (string.IsNullOrEmpty(materialName)) + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex}"; + else + meshNames[lodIndex][meshIndex] = $"Mesh {meshIndex} ({materialName})"; + } + } + } + else + return; // Not supported model type + + // Mesh reference picker + _meshPicker = layout.Custom(); + _meshPicker.CustomControl.MeshNames = meshNames; + _meshPicker.CustomControl.Value = (ModelInstanceActor.MeshReference)Values[0]; + _meshPicker.CustomControl.ValueChanged += () => SetValue(_meshPicker.CustomControl.Value); + } + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (_actorPicker != null) + { + GetActor(out var actor); + _actorPicker.CustomControl.Value = actor; + if (actor != _actor) + { + RebuildLayout(); + return; + } + } + if (_meshPicker != null) + { + _meshPicker.CustomControl.Value = (ModelInstanceActor.MeshReference)Values[0]; + } + } + + private bool GetActor(out ModelInstanceActor actor) + { + actor = null; + foreach (ModelInstanceActor.MeshReference value in Values) + { + if (actor == null) + actor = value.Actor; + else if (actor != value.Actor) + return true; + } + return false; + } + } +} diff --git a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs index 03d5ddd55..a6c4e6623 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -20,7 +20,7 @@ public class MissingScriptEditor : GenericEditor } dropPanel.HeaderTextColor = Color.OrangeRed; - + base.Initialize(layout); } } diff --git a/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs b/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs index 5a10b9a52..c4b334b3a 100644 --- a/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/RagdollEditor.cs @@ -50,7 +50,7 @@ namespace FlaxEditor.CustomEditors.Dedicated grid.Button("Remove bone").Button.ButtonClicked += OnRemoveBone; } - if (Presenter.Owner is Windows.PropertiesWindow || Presenter.Owner is Windows.Assets.PrefabWindow) + if (Presenter.Owner != null) { // Selection var grid = editorGroup.CustomContainer(); @@ -309,10 +309,7 @@ namespace FlaxEditor.CustomEditors.Dedicated if (node != null) selection.Add(node); } - if (Presenter.Owner is Windows.PropertiesWindow propertiesWindow) - propertiesWindow.Editor.SceneEditing.Select(selection); - else if (Presenter.Owner is Windows.Assets.PrefabWindow prefabWindow) - prefabWindow.Select(selection); + Presenter.Owner.Select(selection); } } } diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index aaf45bc52..d7bfbbad7 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -258,27 +258,15 @@ namespace FlaxEditor.CustomEditors.Dedicated /// Small image control added per script group that allows to drag and drop a reference to it. Also used to reorder the scripts. /// /// - internal class ScriptDragIcon : Image + internal class DragImage : Image { - private ScriptsEditor _editor; private bool _isMouseDown; private Float2 _mouseDownPos; /// - /// Gets the target script. + /// Action called when drag event should start. /// - public Script Script => (Script)Tag; - - /// - /// Initializes a new instance of the class. - /// - /// The script editor. - /// The target script. - public ScriptDragIcon(ScriptsEditor editor, Script script) - { - Tag = script; - _editor = editor; - } + public Action Drag; /// public override void OnMouseEnter(Float2 location) @@ -291,11 +279,10 @@ namespace FlaxEditor.CustomEditors.Dedicated /// public override void OnMouseLeave() { - // Check if start drag drop if (_isMouseDown) { - DoDrag(); _isMouseDown = false; + Drag(this); } base.OnMouseLeave(); @@ -304,11 +291,10 @@ namespace FlaxEditor.CustomEditors.Dedicated /// public override void OnMouseMove(Float2 location) { - // Check if start drag drop if (_isMouseDown && Float2.Distance(location, _mouseDownPos) > 10.0f) { - DoDrag(); _isMouseDown = false; + Drag(this); } base.OnMouseMove(location); @@ -319,8 +305,8 @@ namespace FlaxEditor.CustomEditors.Dedicated { if (button == MouseButton.Left) { - // Clear flag _isMouseDown = false; + return true; } return base.OnMouseUp(location, button); @@ -331,21 +317,13 @@ namespace FlaxEditor.CustomEditors.Dedicated { if (button == MouseButton.Left) { - // Set flag _isMouseDown = true; _mouseDownPos = location; + return true; } return base.OnMouseDown(location, button); } - - private void DoDrag() - { - var script = Script; - _editor.OnScriptDragChange(true, script); - DoDragDrop(DragScripts.GetDragData(script)); - _editor.OnScriptDragChange(false, script); - } } internal class ScriptArrangeBar : Control @@ -643,7 +621,7 @@ namespace FlaxEditor.CustomEditors.Dedicated _scriptToggles[i] = scriptToggle; // Add drag button to the group - var scriptDrag = new ScriptDragIcon(this, script) + var scriptDrag = new DragImage { TooltipText = "Script reference", AutoFocus = true, @@ -654,6 +632,13 @@ namespace FlaxEditor.CustomEditors.Dedicated Margin = new Margin(1), Brush = new SpriteBrush(Editor.Instance.Icons.DragBar12), Tag = script, + Drag = img => + { + var s = (Script)img.Tag; + OnScriptDragChange(true, s); + img.DoDragDrop(DragScripts.GetDragData(s)); + OnScriptDragChange(false, s); + } }; // Add settings button to the group diff --git a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs index 03d58ef33..7b9b65c5c 100644 --- a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs @@ -348,7 +348,7 @@ namespace FlaxEditor.CustomEditors.Dedicated if (!CanEditTangent()) return; - + var index = _lastPointSelected.Index; var currentTangentInPosition = _selectedSpline.GetSplineLocalTangent(index, true).Translation; var currentTangentOutPosition = _selectedSpline.GetSplineLocalTangent(index, false).Translation; diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 4aa02ac78..4c153e759 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -88,7 +88,7 @@ namespace FlaxEditor.CustomEditors.Editors LinkValues = Editor.Instance.Windows.PropertiesWin.ScaleLinked; // Add button with the link icon - + _linkButton = new Button { BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Link32), diff --git a/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs new file mode 100644 index 000000000..8ac6a51cb --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/BehaviorKnowledgeSelectorEditor.cs @@ -0,0 +1,196 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using FlaxEditor.GUI; +using FlaxEditor.GUI.Tree; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Utilities; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Custom editor for and . + /// + public sealed class BehaviorKnowledgeSelectorEditor : CustomEditor + { + private ClickableLabel _label; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + _label = layout.ClickableLabel(Path).CustomControl; + _label.RightClick += ShowPicker; + var button = new Button + { + Size = new Float2(16.0f), + Text = "...", + TooltipText = "Edit...", + Parent = _label, + }; + button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true); + button.Clicked += ShowPicker; + } + + /// + public override void Refresh() + { + base.Refresh(); + + // Update label + _label.Text = _label.TooltipText = Path; + } + + private string Path + { + get + { + var v = Values[0]; + if (v is BehaviorKnowledgeSelectorAny any) + return any.Path; + if (v is string str) + return str; + var pathField = v.GetType().GetField("Path"); + return pathField.GetValue(v) as string; + } + set + { + if (string.Equals(Path, value, StringComparison.Ordinal)) + return; + var v = Values[0]; + if (v is BehaviorKnowledgeSelectorAny) + v = new BehaviorKnowledgeSelectorAny(value); + else if (v is string) + v = value; + else + { + var pathField = v.GetType().GetField("Path"); + pathField.SetValue(v, value); + } + SetValue(v); + } + } + + private void ShowPicker() + { + // Get Behavior Knowledge to select from + var behaviorTreeWindow = Presenter.Owner as Windows.Assets.BehaviorTreeWindow; + var rootNode = behaviorTreeWindow?.RootNode; + if (rootNode == null) + return; + var typed = ScriptType.Null; + var valueType = Values[0].GetType(); + if (valueType.Name == "BehaviorKnowledgeSelector`1") + { + // Get typed selector type to show only assignable items + typed = new ScriptType(valueType.GenericTypeArguments[0]); + } + + // Get customization options + var attributes = Values.GetAttributes(); + var attribute = (BehaviorKnowledgeSelectorAttribute)attributes?.FirstOrDefault(x => x is BehaviorKnowledgeSelectorAttribute); + bool isGoalSelector = false; + if (attribute != null) + { + isGoalSelector = attribute.IsGoalSelector; + } + + // Create menu with tree-like structure and search box + var menu = Utilities.Utils.CreateSearchPopup(out var searchBox, out var tree, 0, true); + var selected = Path; + + // Empty + var noneNode = new TreeNode + { + Text = "", + TooltipText = "Deselect value", + Parent = tree, + }; + if (string.IsNullOrEmpty(selected)) + tree.Select(noneNode); + + if (!isGoalSelector) + { + // Blackboard + SetupPickerTypeItems(tree, typed, selected, "Blackboard", "Blackboard/", rootNode.BlackboardType); + } + + // Goals + var goalTypes = rootNode.GoalTypes; + if (goalTypes?.Length != 0) + { + var goalsNode = new TreeNode + { + Text = "Goal", + TooltipText = "List of goal types defined in Blackboard Tree", + Parent = tree, + }; + foreach (var goalTypeName in goalTypes) + { + var goalType = TypeUtils.GetType(goalTypeName); + if (goalType == null) + continue; + var goalTypeNode = SetupPickerTypeItems(tree, typed, selected, goalType.Name, "Goal/" + goalTypeName + "/", goalTypeName, !isGoalSelector); + goalTypeNode.Parent = goalsNode; + } + goalsNode.ExpandAll(true); + } + + tree.SelectedChanged += delegate(List before, List after) + { + if (after.Count == 1) + { + menu.Hide(); + Path = after[0].Tag as string; + } + }; + menu.Show(_label, new Float2(0, _label.Height)); + } + + private TreeNode SetupPickerTypeItems(Tree tree, ScriptType typed, string selected, string text, string typePath, string typeName, bool addItems = true) + { + var type = TypeUtils.GetType(typeName); + if (type == null) + return null; + var typeNode = new TreeNode + { + Text = text, + TooltipText = type.TypeName, + Tag = typePath, // Ability to select whole item type data (eg. whole blackboard value) + Parent = tree, + }; + if (typed && !typed.IsAssignableFrom(type)) + typeNode.Tag = null; + if (string.Equals(selected, (string)typeNode.Tag, StringComparison.Ordinal)) + tree.Select(typeNode); + if (addItems) + { + var items = GenericEditor.GetItemsForType(type, type.IsClass, true); + foreach (var item in items) + { + if (typed && !typed.IsAssignableFrom(item.Info.ValueType)) + continue; + var itemPath = typePath + item.Info.Name; + var node = new TreeNode + { + Text = item.DisplayName, + TooltipText = item.TooltipText, + Tag = itemPath, + Parent = typeNode, + }; + if (string.Equals(selected, itemPath, StringComparison.Ordinal)) + tree.Select(node); + // TODO: add support for nested items (eg. field from blackboard structure field) + } + typeNode.Expand(true); + } + return typeNode; + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/GenericEditor.cs b/Source/Editor/CustomEditors/Editors/GenericEditor.cs index cba2b5a5a..55ac453a9 100644 --- a/Source/Editor/CustomEditors/Editors/GenericEditor.cs +++ b/Source/Editor/CustomEditors/Editors/GenericEditor.cs @@ -26,7 +26,7 @@ namespace FlaxEditor.CustomEditors.Editors /// Describes object property/field information for custom editors pipeline. /// /// - protected class ItemInfo : IComparable + public class ItemInfo : IComparable { private Options.GeneralOptions.MembersOrder _membersOrder; @@ -248,7 +248,7 @@ namespace FlaxEditor.CustomEditors.Editors /// True if use type properties. /// True if use type fields. /// The items. - protected List GetItemsForType(ScriptType type, bool useProperties, bool useFields) + public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields) { var items = new List(); diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs index 4d0c0f662..0369d679d 100644 --- a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs @@ -125,7 +125,7 @@ namespace FlaxEditor.CustomEditors.Editors } // Value - var values = new CustomValueContainer(type, (instance, index) => instance, (instance, index, value) => { }); + var values = new CustomValueContainer(type, (instance, index) => instance); values.AddRange(Values); var editor = CustomEditorsUtil.CreateEditor(type); var style = editor.Style; @@ -160,7 +160,6 @@ namespace FlaxEditor.CustomEditors.Editors var option = _options[comboBox.SelectedIndex]; if (option.Type != null) value = option.Creator(option.Type); - } SetValue(value); RebuildLayoutOnRefresh(); diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs index 911397785..dbd5d124c 100644 --- a/Source/Editor/CustomEditors/Editors/TagEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs @@ -634,7 +634,7 @@ namespace FlaxEditor.CustomEditors.Editors var textSize = FlaxEngine.GUI.Style.Current.FontMedium.MeasureText(buttonText); if (textSize.Y > button.Width) button.Width = textSize.Y + 2; - + button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true); button.Clicked += ShowPicker; } diff --git a/Source/Editor/CustomEditors/Editors/TypeEditor.cs b/Source/Editor/CustomEditors/Editors/TypeEditor.cs index ab17c4ae1..38800a738 100644 --- a/Source/Editor/CustomEditors/Editors/TypeEditor.cs +++ b/Source/Editor/CustomEditors/Editors/TypeEditor.cs @@ -464,6 +464,11 @@ namespace FlaxEditor.CustomEditors.Editors /// public class TypeNameEditor : TypeEditorBase { + /// + /// Prevents spamming log if Value contains missing type to skip research in subsequential Refresh ticks. + /// + private string _lastTypeNameError; + /// public override void Initialize(LayoutElementsContainer layout) { @@ -484,8 +489,19 @@ namespace FlaxEditor.CustomEditors.Editors { base.Refresh(); - if (!HasDifferentValues && Values[0] is string asTypename) - _element.CustomControl.Value = TypeUtils.GetType(asTypename); + if (!HasDifferentValues && Values[0] is string asTypename && + !string.Equals(asTypename, _lastTypeNameError, StringComparison.Ordinal)) + { + try + { + _element.CustomControl.Value = TypeUtils.GetType(asTypename); + } + finally + { + if (_element.CustomControl.Value == null && asTypename.Length != 0) + _lastTypeNameError = asTypename; + } + } } } } diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs index 2b2f0a3d3..93aacbd34 100644 --- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs +++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs @@ -175,7 +175,7 @@ namespace FlaxEditor.CustomEditors.GUI { // Clear flag _mouseOverSplitter = false; - + if (_cursorChanged) { Cursor = CursorType.Default; diff --git a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs index 5be61399b..9a09c4cc2 100644 --- a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs +++ b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs @@ -38,15 +38,12 @@ namespace FlaxEditor.CustomEditors /// /// Type of the value. /// The value getter. - /// The value setter. + /// The value setter (can be null if value is read-only). /// The custom type attributes used to override the value editor logic or appearance (eg. instance of ). - public CustomValueContainer(ScriptType valueType, GetDelegate getter, SetDelegate setter, object[] attributes = null) + public CustomValueContainer(ScriptType valueType, GetDelegate getter, SetDelegate setter = null, object[] attributes = null) : base(ScriptMemberInfo.Null, valueType) { - if (getter == null || setter == null) - throw new ArgumentNullException(); - - _getter = getter; + _getter = getter ?? throw new ArgumentNullException(); _setter = setter; _attributes = attributes; } @@ -57,9 +54,9 @@ namespace FlaxEditor.CustomEditors /// Type of the value. /// The initial value. /// The value getter. - /// The value setter. + /// The value setter (can be null if value is read-only). /// The custom type attributes used to override the value editor logic or appearance (eg. instance of ). - public CustomValueContainer(ScriptType valueType, object initialValue, GetDelegate getter, SetDelegate setter, object[] attributes = null) + public CustomValueContainer(ScriptType valueType, object initialValue, GetDelegate getter, SetDelegate setter = null, object[] attributes = null) : this(valueType, getter, setter, attributes) { Add(initialValue); @@ -89,6 +86,8 @@ namespace FlaxEditor.CustomEditors { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); + if (_setter == null) + return; for (int i = 0; i < Count; i++) { @@ -105,6 +104,8 @@ namespace FlaxEditor.CustomEditors throw new ArgumentException(); if (values == null || values.Count != Count) throw new ArgumentException(); + if (_setter == null) + return; for (int i = 0; i < Count; i++) { @@ -120,6 +121,8 @@ namespace FlaxEditor.CustomEditors { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); + if (_setter == null) + return; for (int i = 0; i < Count; i++) { diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 06a6f0979..1b46222ea 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -8,7 +8,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; using FlaxEditor.Content; -using FlaxEditor.Content.Import; using FlaxEditor.Content.Settings; using FlaxEditor.Content.Thumbnails; using FlaxEditor.Modules; @@ -154,12 +153,12 @@ namespace FlaxEditor public ContentFindingModule ContentFinding; /// - /// The scripts editing + /// The scripts editing. /// public CodeEditingModule CodeEditing; /// - /// The scripts documentation + /// The scripts documentation. /// public CodeDocsModule CodeDocs; @@ -179,7 +178,7 @@ namespace FlaxEditor public ProjectCacheModule ProjectCache; /// - /// The undo/redo + /// The undo/redo. /// public EditorUndo Undo; @@ -726,8 +725,8 @@ namespace FlaxEditor // Cleanup Undo.Dispose(); - Surface.VisualScriptSurface.NodesCache.Clear(); - Surface.AnimGraphSurface.NodesCache.Clear(); + foreach (var cache in Surface.VisjectSurface.NodesCache.Caches.ToArray()) + cache.Clear(); Instance = null; // Invoke new instance if need to open a project @@ -797,7 +796,6 @@ namespace FlaxEditor { if (projectFilePath == null || !File.Exists(projectFilePath)) { - // Error MessageBox.Show("Missing project"); return; } @@ -933,21 +931,11 @@ namespace FlaxEditor /// The . /// Animation = 11, - } - /// - /// Imports the audio asset file to the target location. - /// - /// The source file path. - /// The result asset file path. - /// The settings. - /// True if importing failed, otherwise false. - public static bool Import(string inputPath, string outputPath, AudioImportSettings settings) - { - if (settings == null) - throw new ArgumentNullException(); - settings.ToInternal(out var internalOptions); - return Internal_ImportAudio(inputPath, outputPath, ref internalOptions); + /// + /// The . + /// + BehaviorTree = 12, } /// @@ -1667,10 +1655,6 @@ namespace FlaxEditor [return: MarshalAs(UnmanagedType.U1)] internal static partial bool Internal_CloneAssetFile(string dstPath, string srcPath, ref Guid dstId); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_ImportAudio", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] - [return: MarshalAs(UnmanagedType.U1)] - internal static partial bool Internal_ImportAudio(string inputPath, string outputPath, ref AudioImportSettings.InternalOptions options); - [LibraryImport("FlaxEngine", EntryPoint = "EditorInternal_GetAudioClipMetadata", StringMarshalling = StringMarshalling.Custom, StringMarshallingCustomType = typeof(StringMarshaller))] internal static partial void Internal_GetAudioClipMetadata(IntPtr obj, out int originalSize, out int importedSize); diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 7ecd197eb..628af18e1 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -154,6 +154,7 @@ namespace FlaxEditor.GUI.ContextMenu } // Unlock and perform controls update + Location = Float2.Zero; UnlockChildrenRecursive(); PerformLayout(); @@ -162,7 +163,6 @@ namespace FlaxEditor.GUI.ContextMenu var dpiSize = Size * dpiScale; var locationWS = parent.PointToWindow(location); var locationSS = parentWin.PointToScreen(locationWS); - Location = Float2.Zero; var monitorBounds = Platform.GetMonitorBounds(locationSS); var rightBottomLocationSS = locationSS + dpiSize; bool isUp = false, isLeft = false; diff --git a/Source/Editor/GUI/Drag/DragScripts.cs b/Source/Editor/GUI/Drag/DragScripts.cs index e72d28479..875875ef3 100644 --- a/Source/Editor/GUI/Drag/DragScripts.cs +++ b/Source/Editor/GUI/Drag/DragScripts.cs @@ -58,7 +58,6 @@ namespace FlaxEditor.GUI.Drag { if (item == null) throw new ArgumentNullException(); - return new DragDataText(DragPrefix + item.ID.ToString("N")); } @@ -71,11 +70,9 @@ namespace FlaxEditor.GUI.Drag { if (items == null) throw new ArgumentNullException(); - string text = DragPrefix; foreach (var item in items) text += item.ID.ToString("N") + '\n'; - return new DragDataText(text); } @@ -83,9 +80,7 @@ namespace FlaxEditor.GUI.Drag /// Tries to parse the drag data. /// /// The data. - /// - /// Gathered objects or empty IEnumerable if cannot get any valid. - /// + /// Gathered objects or empty IEnumerable if cannot get any valid. public override IEnumerable