diff --git a/.gitignore b/.gitignore index 54907892f..b7e11e554 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Source/*.csproj /Package_*/ !Source/Engine/Debug /Source/Platforms/Editor/Linux/Mono/etc/mono/registry +PackageEditor_Cert.command PackageEditor_Cert.bat PackagePlatforms_Cert.bat diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax index 1335a84f4..b42c2f325 100644 --- a/Content/Editor/Particles/Smoke.flax +++ b/Content/Editor/Particles/Smoke.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3dc51e7805056006ca6cbb481ba202583a9b2287c152fc04e28e1d07747d6ce -size 14706 +oid sha256:334ac0d00495fc88b10839061ff0c3f45323d4f75ab6176b19005199a7324a19 +size 14569 diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax index 7977e231b..7ee0ed6e9 100644 --- a/Content/Editor/Particles/Sparks.flax +++ b/Content/Editor/Particles/Sparks.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d902ab5f79426cc66dc5f19a3b8280136a58aa3c6fd317554d1a032357c65a -size 15275 +oid sha256:87046a9bfe275cac290b4764de8a512c222ccc386d01af9026d57c3e4b7773b6 +size 13625 diff --git a/Content/Editor/Scripting/ScriptTemplate.cs b/Content/Editor/Scripting/ScriptTemplate.cs index 663acf05f..30fbe9d86 100644 --- a/Content/Editor/Scripting/ScriptTemplate.cs +++ b/Content/Editor/Scripting/ScriptTemplate.cs @@ -33,4 +33,3 @@ public class %class% : Script // Here you can add code that needs to be called every frame } } - diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 18f653f86..0e94c5c06 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c8aa181a814d69b15ffec6493a71a6f42ae816ce04f7803cff2d5073b4b3c4f -size 11790 +oid sha256:e075583620e62407503c73f52487c204ddcad421e80fa621aebd55af1cfb08d5 +size 11798 diff --git a/Flax.flaxproj b/Flax.flaxproj index fbf48cc2b..a3157a032 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -2,8 +2,9 @@ "Name": "Flax", "Version": { "Major": 1, - "Minor": 6, - "Build": 6345 + "Minor": 7, + "Revision": 0, + "Build": 6404 }, "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/GenerateProjectFiles.bat b/GenerateProjectFiles.bat index 622939c34..28970a203 100644 --- a/GenerateProjectFiles.bat +++ b/GenerateProjectFiles.bat @@ -15,7 +15,7 @@ if errorlevel 1 goto BuildToolFailed :: Build bindings for all editor configurations echo Building C# bindings... -Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor,FlaxGame +Binaries\Tools\Flax.Build.exe -build -BuildBindingsOnly -arch=x64 -platform=Windows --buildTargets=FlaxEditor popd echo Done! diff --git a/GenerateProjectFiles.command b/GenerateProjectFiles.command index 5ee5c0783..a42121252 100755 --- a/GenerateProjectFiles.command +++ b/GenerateProjectFiles.command @@ -14,4 +14,4 @@ bash ./Development/Scripts/Mac/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=ARM64 -platform=Mac --buildTargets=FlaxEditor diff --git a/GenerateProjectFiles.sh b/GenerateProjectFiles.sh index dceb8abe8..76d96c7ef 100755 --- a/GenerateProjectFiles.sh +++ b/GenerateProjectFiles.sh @@ -14,4 +14,4 @@ bash ./Development/Scripts/Linux/CallBuildTool.sh --genproject "$@" # Build bindings for all editor configurations echo Building C# bindings... # TODO: Detect the correct architecture here -Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor,FlaxGame +Binaries/Tools/Flax.Build -build -BuildBindingsOnly -arch=x64 -platform=Linux --buildTargets=FlaxEditor diff --git a/README.md b/README.md index fac631a6a..d6688bd03 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Flax Engine is a high quality modern 3D game engine written in C++ and C#. -From stunning graphics to powerful scripts - Flax can give everything for your games. Designed for fast workflow with many ready to use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). +From stunning graphics to powerful scripts, it's designed for fast workflow with many ready-to-use features waiting for you right now. To learn more see the website ([www.flaxengine.com](https://flaxengine.com)). This repository contains full source code of the Flax Engine (excluding NDA-protected platforms support). Anyone is welcome to contribute or use the modified source in Flax-based games. diff --git a/Source/Editor/Content/AssetPickerValidator.cs b/Source/Editor/Content/AssetPickerValidator.cs new file mode 100644 index 000000000..f43ab6a29 --- /dev/null +++ b/Source/Editor/Content/AssetPickerValidator.cs @@ -0,0 +1,292 @@ +using System; +using System.IO; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.Utilities; + +namespace FlaxEditor.Content; + +/// +/// Manages and converts the selected content item to the appropriate types. Useful for drag operations. +/// +public class AssetPickerValidator : IContentItemOwner +{ + private Asset _selected; + private ContentItem _selectedItem; + private ScriptType _type; + private string _fileExtension; + + /// + /// Gets or sets the selected item. + /// + public ContentItem SelectedItem + { + get => _selectedItem; + set + { + if (_selectedItem == value) + return; + if (value == null) + { + if (_selected == null && _selectedItem is SceneItem) + { + // Deselect scene reference + _selectedItem.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + return; + } + + // Deselect + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + OnSelectedItemChanged(); + } + else if (value is SceneItem item) + { + if (_selectedItem == item) + return; + if (!IsValid(item)) + item = null; + + // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = null; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + else if (value is AssetItem assetItem) + { + SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); + } + else + { + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = value; + _selected = null; + OnSelectedItemChanged(); + } + } + } + + /// + /// Gets or sets the selected asset identifier. + /// + public Guid SelectedID + { + get + { + if (_selected != null) + return _selected.ID; + if (_selectedItem is AssetItem assetItem) + return assetItem.ID; + return Guid.Empty; + } + set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); + } + + /// + /// Gets or sets the selected content item path. + /// + 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); + } + } + } + + /// + /// Gets or sets the selected asset object. + /// + public Asset SelectedAsset + { + get => _selected; + set + { + // Check if value won't change + if (value == _selected) + return; + + // Find item from content database and check it + var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; + if (item != null && !IsValid(item)) + item = null; + + // Change value + _selectedItem?.RemoveReference(this); + _selectedItem = item; + _selected = value; + _selectedItem?.AddReference(this); + OnSelectedItemChanged(); + } + } + + /// + /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. + /// + public ScriptType AssetType + { + get => _type; + set + { + if (_type != value) + { + _type = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Gets or sets the content items extensions filter. Null if unused. + /// + public string FileExtension + { + get => _fileExtension; + set + { + if (_fileExtension != value) + { + _fileExtension = value; + + // Auto deselect if the current value is invalid + if (_selectedItem != null && !IsValid(_selectedItem)) + SelectedItem = null; + } + } + } + + /// + /// Occurs when selected item gets changed. + /// + public event Action SelectedItemChanged; + + /// + /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick. + /// + public Func CheckValid; + + /// + /// Returns whether item is valid. + /// + /// + /// + public bool IsValid(ContentItem item) + { + if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) + return false; + if (CheckValid != null && !CheckValid(item)) + return false; + if (_type == ScriptType.Null) + return true; + + if (item is AssetItem assetItem) + { + // Faster path for binary items (in-built) + if (assetItem is BinaryAssetItem binaryItem) + return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); + + // Type filter + var type = TypeUtils.GetType(assetItem.TypeName); + if (_type.IsAssignableFrom(type)) + return true; + + // Json assets can contain any type of the object defined by the C# type (data oriented design) + if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) + return true; + + // Special case for scene asset references + if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) + return true; + } + + return false; + } + + /// + /// Initializes a new instance of the class. + /// + public AssetPickerValidator() + : this(new ScriptType(typeof(Asset))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The assets types that this picker accepts. + public AssetPickerValidator(ScriptType assetType) + { + _type = assetType; + } + + /// + /// Called when selected item gets changed. + /// + protected virtual void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(); + } + + /// + public void OnItemDeleted(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + public void OnItemRenamed(ContentItem item) + { + } + + /// + public void OnItemReimported(ContentItem item) + { + } + + /// + public void OnItemDispose(ContentItem item) + { + // Deselect item + SelectedItem = null; + } + + /// + /// Call to remove reference from the selected item. + /// + public void OnDestroy() + { + _selectedItem?.RemoveReference(this); + _selectedItem = null; + _selected = null; + } +} diff --git a/Source/Editor/Content/GUI/ContentView.cs b/Source/Editor/Content/GUI/ContentView.cs index 5e054e5c6..259be104b 100644 --- a/Source/Editor/Content/GUI/ContentView.cs +++ b/Source/Editor/Content/GUI/ContentView.cs @@ -220,8 +220,9 @@ namespace FlaxEditor.Content.GUI // Remove references and unlink items for (int i = 0; i < _items.Count; i++) { - _items[i].Parent = null; - _items[i].RemoveReference(this); + var item = _items[i]; + item.Parent = null; + item.RemoveReference(this); } _items.Clear(); @@ -263,11 +264,12 @@ namespace FlaxEditor.Content.GUI // Add references and link items for (int i = 0; i < items.Count; i++) { - if (items[i].Visible) + var item = items[i]; + if (item.Visible && !_items.Contains(item)) { - items[i].Parent = this; - items[i].AddReference(this); - _items.Add(items[i]); + item.Parent = this; + item.AddReference(this); + _items.Add(item); } } if (selection != null) @@ -279,6 +281,8 @@ namespace FlaxEditor.Content.GUI // Sort items depending on sortMethod parameter _children.Sort(((control, control1) => { + if (control == null || control1 == null) + return 0; if (sortType == SortType.AlphabeticReverse) { if (control.CompareTo(control1) > 0) @@ -520,8 +524,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/Items/ContentItem.cs b/Source/Editor/Content/Items/ContentItem.cs index 6bbdc0a51..604caa704 100644 --- a/Source/Editor/Content/Items/ContentItem.cs +++ b/Source/Editor/Content/Items/ContentItem.cs @@ -323,8 +323,6 @@ namespace FlaxEditor.Content /// The new path. internal virtual void UpdatePath(string value) { - Assert.AreNotEqual(Path, value); - // Set path Path = StringUtils.NormalizePath(value); FileName = System.IO.Path.GetFileName(value); 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/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 78d88b440..004c2aed7 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -30,6 +30,12 @@ namespace FlaxEditor.Content return item is SceneItem; } + /// + public override bool AcceptsAsset(string typeName, string path) + { + return (typeName == Scene.AssetTypename || typeName == Scene.EditorPickerTypename) && path.EndsWith(FileExtension, StringComparison.OrdinalIgnoreCase); + } + /// public override bool CanCreate(ContentFolder targetLocation) { 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..1282e4daa 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 /// @@ -338,18 +406,16 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < maxChecks; i++) { var request = _requests[i]; - try { if (request.IsReady) - { return request; - } } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } @@ -368,7 +434,6 @@ namespace FlaxEditor.Content.Thumbnails // Create atlas if (PreviewsCache.Create(path)) { - // Error Editor.LogError("Failed to create thumbnails atlas."); return null; } @@ -377,7 +442,6 @@ namespace FlaxEditor.Content.Thumbnails var atlas = FlaxEngine.Content.LoadAsync(path); if (atlas == null) { - // Error Editor.LogError("Failed to load thumbnails atlas."); return null; } @@ -449,7 +513,6 @@ namespace FlaxEditor.Content.Thumbnails for (int i = 0; i < checks; i++) { var request = _requests[i]; - try { if (request.IsReady) @@ -463,8 +526,9 @@ namespace FlaxEditor.Content.Thumbnails } catch (Exception ex) { - Editor.LogWarning(ex); Editor.LogWarning($"Failed to prepare thumbnail rendering for {request.Item.ShortName}."); + Editor.LogWarning(ex); + _requests.RemoveAt(i--); } } diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp index ec7b8e7e1..42ef6fcdf 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.cpp @@ -104,4 +104,19 @@ bool LinuxPlatformTools::OnDeployBinaries(CookingData& data) return false; } +void LinuxPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h index 562b38962..432240d00 100644 --- a/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Linux/LinuxPlatformTools.h @@ -20,6 +20,7 @@ public: ArchitectureType GetArchitecture() const override; bool UseSystemDotnet() const override; bool OnDeployBinaries(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp index 0008228f7..aa56a6b95 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.cpp @@ -17,9 +17,7 @@ #include "Editor/ProjectInfo.h" #include "Editor/Cooker/GameCooker.h" #include "Editor/Utilities/EditorUtilities.h" - -#include "pugixml/pugixml_extra.hpp" - +#include using namespace pugi; IMPLEMENT_SETTINGS_GETTER(MacPlatformSettings, MacPlatform); @@ -251,4 +249,19 @@ bool MacPlatformTools::OnPostProcess(CookingData& data) return false; } +void MacPlatformTools::OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) +{ + // Pick the first executable file + Array files; + FileSystem::DirectoryGetFiles(files, data.NativeCodeOutputPath, TEXT("*"), DirectorySearchOption::TopDirectoryOnly); + for (auto& file : files) + { + if (FileSystem::GetExtension(file).IsEmpty()) + { + executableFile = file; + break; + } + } +} + #endif diff --git a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h index 21d9141e3..efdd0b733 100644 --- a/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h +++ b/Source/Editor/Cooker/Platform/Mac/MacPlatformTools.h @@ -27,6 +27,7 @@ public: bool IsNativeCodeFile(CookingData& data, const String& file) override; void OnBuildStarted(CookingData& data) override; bool OnPostProcess(CookingData& data) override; + void OnRun(CookingData& data, String& executableFile, String& commandLineFormat, String& workingDir) override; }; #endif diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index c9bbc016e..8306475d1 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -131,7 +131,8 @@ bool DeployDataStep::Perform(CookingData& data) if (FileSystem::DirectoryExists(dstDotnet)) { String cachedData; - File::ReadAllText(dotnetCacheFilePath, cachedData); + if (FileSystem::FileExists(dotnetCacheFilePath)) + File::ReadAllText(dotnetCacheFilePath, cachedData); if (cachedData != dotnetCachedValue) { FileSystem::DeleteDirectory(dstDotnet); @@ -360,7 +361,7 @@ bool DeployDataStep::Perform(CookingData& data) data.AddRootEngineAsset(PRE_INTEGRATED_GF_ASSET_NAME); data.AddRootEngineAsset(SMAA_AREA_TEX); data.AddRootEngineAsset(SMAA_SEARCH_TEX); - if (data.Configuration != BuildConfiguration::Release) + if (!buildSettings.SkipDefaultFonts) data.AddRootEngineAsset(TEXT("Editor/Fonts/Roboto-Regular")); // Register custom assets (eg. plugins) diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index 03b21e12b..32111e51c 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -157,6 +157,12 @@ namespace FlaxEditor.CustomEditors var values = _values; var presenter = _presenter; var layout = _layout; + if (layout.Editors.Count != 1) + { + // There are more editors using the same layout so rebuild parent editor to prevent removing others editors + _parent?.RebuildLayout(); + return; + } var control = layout.ContainerControl; var parent = _parent; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; 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..4053c6e16 --- /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..4099e5aee --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/MeshReferenceEditor.cs @@ -0,0 +1,349 @@ +// 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; + + if (ParentEditor.Values.Any(x => x is Cloth)) + { + // Cloth always picks the parent model mesh + if (actor == null) + { + layout.Label("Cloth needs to be added as a child to model actor."); + } + } + else + { + // 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()) + { + layout.Label("No model."); + 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()) + { + layout.Label("No model."); + 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 a6c4e6623..8fb742b5e 100644 --- a/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/MissingScriptEditor.cs @@ -1,6 +1,13 @@ -using FlaxEditor.CustomEditors.Editors; +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using FlaxEditor.Actions; +using FlaxEditor.CustomEditors.Editors; +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; +using System.Collections.Generic; namespace FlaxEditor.CustomEditors.Dedicated; @@ -10,6 +17,10 @@ namespace FlaxEditor.CustomEditors.Dedicated; [CustomEditor(typeof(MissingScript)), DefaultEditor] public class MissingScriptEditor : GenericEditor { + private DropPanel _dropPanel; + private Button _replaceScriptButton; + private CheckBox _shouldReplaceAllCheckbox; + /// public override void Initialize(LayoutElementsContainer layout) { @@ -18,9 +29,137 @@ public class MissingScriptEditor : GenericEditor base.Initialize(layout); return; } + _dropPanel = dropPanel; + _dropPanel.HeaderTextColor = Color.OrangeRed; - dropPanel.HeaderTextColor = Color.OrangeRed; + var replaceScriptPanel = new Panel + { + Parent = _dropPanel, + Height = 64, + }; + + _replaceScriptButton = new Button + { + Text = "Replace Script", + TooltipText = "Replaces the missing script with a given script type", + AnchorPreset = AnchorPresets.TopCenter, + Width = 240, + Height = 24, + X = -120, + Y = 0, + Parent = replaceScriptPanel, + }; + _replaceScriptButton.Clicked += OnReplaceScriptButtonClicked; + + var replaceAllLabel = new Label + { + Text = "Replace all matching missing scripts", + TooltipText = "Whether or not to apply this script change to all scripts missing the same type.", + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + replaceAllLabel.X -= FlaxEngine.GUI.Style.Current.FontSmall.MeasureText(replaceAllLabel.Text).X; + + _shouldReplaceAllCheckbox = new CheckBox + { + TooltipText = replaceAllLabel.TooltipText, + AnchorPreset = AnchorPresets.BottomCenter, + Y = -34, + Parent = replaceScriptPanel, + }; + + float centerDifference = (_shouldReplaceAllCheckbox.Right - replaceAllLabel.Left) / 2; + replaceAllLabel.X += centerDifference; + _shouldReplaceAllCheckbox.X += centerDifference; base.Initialize(layout); } + + private void FindActorsWithMatchingMissingScript(List missingScripts) + { + foreach (Actor actor in Level.GetActors(typeof(Actor))) + { + for (int scriptIndex = 0; scriptIndex < actor.ScriptsCount; scriptIndex++) + { + Script actorScript = actor.Scripts[scriptIndex]; + if (actorScript is not MissingScript missingActorScript) + continue; + + MissingScript currentMissing = Values[0] as MissingScript; + if (missingActorScript.MissingTypeName != currentMissing.MissingTypeName) + continue; + + missingScripts.Add(missingActorScript); + } + } + } + + private void RunReplacementMultiCast(List actions) + { + if (actions.Count == 0) + { + Editor.LogWarning("Failed to replace scripts!"); + return; + } + + var multiAction = new MultiUndoAction(actions); + multiAction.Do(); + var presenter = ParentEditor.Presenter; + if (presenter != null) + { + presenter.Undo.AddAction(multiAction); + presenter.Control.Focus(); + } + } + + private void ReplaceScript(ScriptType script, bool replaceAllInScene) + { + var actions = new List(4); + + var missingScripts = new List(); + if (!replaceAllInScene) + missingScripts.Add((MissingScript)Values[0]); + else + FindActorsWithMatchingMissingScript(missingScripts); + + foreach (var missingScript in missingScripts) + actions.Add(AddRemoveScript.Add(missingScript.Actor, script)); + RunReplacementMultiCast(actions); + + for (int actionIdx = 0; actionIdx < actions.Count; actionIdx++) + { + AddRemoveScript addRemoveScriptAction = (AddRemoveScript)actions[actionIdx]; + int orderInParent = addRemoveScriptAction.GetOrderInParent(); + + Script newScript = missingScripts[actionIdx].Actor.Scripts[orderInParent]; + missingScripts[actionIdx].ReferenceScript = newScript; + } + actions.Clear(); + + foreach (var missingScript in missingScripts) + actions.Add(AddRemoveScript.Remove(missingScript)); + RunReplacementMultiCast(actions); + } + + private void OnReplaceScriptButtonClicked() + { + var scripts = Editor.Instance.CodeEditing.Scripts.Get(); + if (scripts.Count == 0) + { + // No scripts + var cm1 = new ContextMenu(); + cm1.AddButton("No scripts in project"); + cm1.Show(_dropPanel, _replaceScriptButton.BottomLeft); + return; + } + + // Show context menu with list of scripts to add + var cm = new ItemsListContextMenu(180); + for (int i = 0; i < scripts.Count; i++) + cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i])); + cm.ItemClicked += item => ReplaceScript((ScriptType)item.Tag, _shouldReplaceAllCheckbox.Checked); + cm.SortItems(); + cm.Show(_dropPanel, _replaceScriptButton.BottomLeft - new Float2((cm.Width - _replaceScriptButton.Width) / 2, 0)); + } } 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/Editors/ActorLayerEditor.cs b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs index bf087feda..4829bd91a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorLayerEditor.cs @@ -34,7 +34,7 @@ namespace FlaxEditor.CustomEditors.Editors value = 0; // If selected is single actor that has children, ask if apply layer to the sub objects as well - if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren) + if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren && !Editor.IsPlayMode) { var valueText = comboBox.SelectedItem; diff --git a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs index cfba940c2..1f3359fd5 100644 --- a/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/AssetRefEditor.cs @@ -71,7 +71,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Generic file picker assetType = ScriptType.Null; - Picker.FileExtension = assetReference.TypeName; + Picker.Validator.FileExtension = assetReference.TypeName; } else { @@ -85,7 +85,7 @@ namespace FlaxEditor.CustomEditors.Editors } } - Picker.AssetType = assetType; + Picker.Validator.AssetType = assetType; Picker.Height = height; Picker.SelectedItemChanged += OnSelectedItemChanged; } @@ -95,15 +95,15 @@ namespace FlaxEditor.CustomEditors.Editors if (_isRefreshing) return; if (typeof(AssetItem).IsAssignableFrom(_valueType.Type)) - SetValue(Picker.SelectedItem); + SetValue(Picker.Validator.SelectedItem); else if (_valueType.Type == typeof(Guid)) - SetValue(Picker.SelectedID); + SetValue(Picker.Validator.SelectedID); else if (_valueType.Type == typeof(SceneReference)) - SetValue(new SceneReference(Picker.SelectedID)); + SetValue(new SceneReference(Picker.Validator.SelectedID)); else if (_valueType.Type == typeof(string)) - SetValue(Picker.SelectedPath); + SetValue(Picker.Validator.SelectedPath); else - SetValue(Picker.SelectedAsset); + SetValue(Picker.Validator.SelectedAsset); } /// @@ -115,15 +115,15 @@ namespace FlaxEditor.CustomEditors.Editors { _isRefreshing = true; if (Values[0] is AssetItem assetItem) - Picker.SelectedItem = assetItem; + Picker.Validator.SelectedItem = assetItem; else if (Values[0] is Guid guid) - Picker.SelectedID = guid; + Picker.Validator.SelectedID = guid; else if (Values[0] is SceneReference sceneAsset) - Picker.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); + Picker.Validator.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID); else if (Values[0] is string path) - Picker.SelectedPath = path; + Picker.Validator.SelectedPath = path; else - Picker.SelectedAsset = Values[0] as Asset; + Picker.Validator.SelectedAsset = Values[0] as Asset; _isRefreshing = false; } } 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/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 8922e2d25..7d92c71c0 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -3,9 +3,12 @@ using System; using System.Collections; using System.Linq; +using FlaxEditor.Content; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Drag; +using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; @@ -135,14 +138,43 @@ namespace FlaxEditor.CustomEditors.Editors spacing = collection.Spacing; } + var dragArea = layout.CustomContainer(); + dragArea.CustomControl.Editor = this; + dragArea.CustomControl.ElementType = ElementType; + + // Check for the AssetReferenceAttribute. In JSON assets, it can be used to filter + // which scripts can be dragged over and dropped on this collection editor. + var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute); + if (assetReference != null) + { + if (string.IsNullOrEmpty(assetReference.TypeName)) + { + } + else if (assetReference.TypeName.Length > 1 && assetReference.TypeName[0] == '.') + { + dragArea.CustomControl.ElementType = ScriptType.Null; + dragArea.CustomControl.FileExtension = assetReference.TypeName; + } + else + { + var customType = TypeUtils.GetType(assetReference.TypeName); + if (customType != ScriptType.Null) + dragArea.CustomControl.ElementType = customType; + else if (!Content.Settings.GameSettings.OptionalPlatformSettings.Contains(assetReference.TypeName)) + Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for drag and drop filter.", assetReference.TypeName)); + else + dragArea.CustomControl.ElementType = ScriptType.Void; + } + } + // Size if (_readOnly || (NotNullItems && size == 0)) { - layout.Label("Size", size.ToString()); + dragArea.Label("Size", size.ToString()); } else { - _size = layout.IntegerValue("Size"); + _size = dragArea.IntegerValue("Size"); _size.IntValue.MinValue = 0; _size.IntValue.MaxValue = ushort.MaxValue; _size.IntValue.Value = size; @@ -152,7 +184,7 @@ namespace FlaxEditor.CustomEditors.Editors // Elements if (size > 0) { - var panel = layout.VerticalPanel(); + var panel = dragArea.VerticalPanel(); panel.Panel.BackgroundColor = _background; var elementType = ElementType; @@ -212,37 +244,33 @@ namespace FlaxEditor.CustomEditors.Editors // Add/Remove buttons if (!_readOnly) { - var area = layout.Space(20); - var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16) - { - Text = "+", - TooltipText = "Add new item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = !NotNullItems || size > 0, - }; - addButton.Clicked += () => - { - if (IsSetBlocked) - return; + var panel = dragArea.HorizontalPanel(); + panel.Panel.Size = new Float2(0, 20); + panel.Panel.Margin = new Margin(2); - Resize(Count + 1); - }; - var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16) - { - Text = "-", - TooltipText = "Remove last item", - AnchorPreset = AnchorPresets.TopRight, - Parent = area.ContainerControl, - Enabled = size > 0, - }; - removeButton.Clicked += () => + var removeButton = panel.Button("-", "Remove last item"); + removeButton.Button.Size = new Float2(16, 16); + removeButton.Button.Enabled = size > 0; + removeButton.Button.AnchorPreset = AnchorPresets.TopRight; + removeButton.Button.Clicked += () => { if (IsSetBlocked) return; Resize(Count - 1); }; + + var addButton = panel.Button("+", "Add new item"); + addButton.Button.Size = new Float2(16, 16); + addButton.Button.Enabled = !NotNullItems || size > 0; + addButton.Button.AnchorPreset = AnchorPresets.TopRight; + addButton.Button.Clicked += () => + { + if (IsSetBlocked) + return; + + Resize(Count + 1); + }; } } @@ -369,5 +397,232 @@ namespace FlaxEditor.CustomEditors.Editors } return base.OnDirty(editor, value, token); } + + private class DragAreaControl : VerticalPanel + { + private DragItems _dragItems; + private DragActors _dragActors; + private DragHandlers _dragHandlers; + private AssetPickerValidator _pickerValidator; + + public ScriptType ElementType + { + get => _pickerValidator?.AssetType ?? ScriptType.Null; + set => _pickerValidator = new AssetPickerValidator(value); + } + + public CollectionEditor Editor { get; set; } + + public string FileExtension + { + set => _pickerValidator.FileExtension = value; + } + + /// + public override void Draw() + { + if (_dragHandlers is { HasValidDrag: true }) + { + var area = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(area, Color.Orange * 0.5f); + Render2D.DrawRectangle(area, Color.Black); + } + + base.Draw(); + } + + public override void OnDestroy() + { + _pickerValidator.OnDestroy(); + } + + private bool ValidateActors(ActorNode node) + { + return node.Actor.GetScript(ElementType.Type) || ElementType.Type.IsAssignableTo(typeof(Actor)); + } + + /// + public override DragDropEffect OnDragEnter(ref Float2 location, DragData data) + { + var result = base.OnDragEnter(ref location, data); + if (result != DragDropEffect.None) + return result; + + if (_dragHandlers == null) + { + _dragItems = new DragItems(_pickerValidator.IsValid); + _dragActors = new DragActors(ValidateActors); + _dragHandlers = new DragHandlers + { + _dragActors, + _dragItems + }; + } + return _dragHandlers.OnDragEnter(data); + } + + /// + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + var result = base.OnDragMove(ref location, data); + if (result != DragDropEffect.None) + return result; + + return _dragHandlers.Effect; + } + + /// + public override void OnDragLeave() + { + _dragHandlers.OnDragLeave(); + + base.OnDragLeave(); + } + + /// + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + var result = base.OnDragDrop(ref location, data); + if (result != DragDropEffect.None) + { + _dragHandlers.OnDragDrop(null); + return result; + } + + if (_dragHandlers.HasValidDrag) + { + if (_dragItems.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragItems.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = _dragItems.Objects[i - oldSize], + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + array.SetValue(validator.SelectedItem, i); + else if (ElementType.Type == typeof(Guid)) + array.SetValue(validator.SelectedID, i); + else if (ElementType.Type == typeof(SceneReference)) + array.SetValue(new SceneReference(validator.SelectedID), i); + else if (ElementType.Type == typeof(string)) + array.SetValue(validator.SelectedPath, i); + else + array.SetValue(validator.SelectedAsset, i); + + validator.OnDestroy(); + } + Editor.SetValue(array); + } + else + { + foreach (var item in _dragItems.Objects) + { + var validator = new AssetPickerValidator + { + FileExtension = _pickerValidator.FileExtension, + AssetType = _pickerValidator.AssetType, + SelectedItem = item, + }; + + if (typeof(AssetItem).IsAssignableFrom(ElementType.Type)) + list.Add(validator.SelectedItem); + else if (ElementType.Type == typeof(Guid)) + list.Add(validator.SelectedID); + else if (ElementType.Type == typeof(SceneReference)) + list.Add(new SceneReference(validator.SelectedID)); + else if (ElementType.Type == typeof(string)) + list.Add(validator.SelectedPath); + else + list.Add(validator.SelectedAsset); + + validator.OnDestroy(); + } + Editor.SetValue(list); + } + } + else if (_dragActors.HasValidDrag) + { + var list = Editor.CloneValues(); + if (list == null) + { + if (Editor.Values.Type.IsArray) + { + list = TypeUtils.CreateArrayInstance(Editor.Values.Type.GetElementType(), 0); + } + else + { + list = Editor.Values.Type.CreateInstance() as IList; + } + } + + if (list.IsFixedSize) + { + var oldSize = list.Count; + var newSize = list.Count + _dragActors.Objects.Count; + var type = Editor.Values.Type.GetElementType(); + var array = TypeUtils.CreateArrayInstance(type, newSize); + list.CopyTo(array, 0); + + for (var i = oldSize; i < newSize; i++) + { + var actor = _dragActors.Objects[i - oldSize].Actor; + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + array.SetValue(actor, i); + } + else + { + array.SetValue(actor.GetScript(ElementType.Type), i); + } + } + Editor.SetValue(array); + } + else + { + foreach (var actorNode in _dragActors.Objects) + { + if (ElementType.Type.IsAssignableTo(typeof(Actor))) + { + list.Add(actorNode.Actor); + } + else + { + list.Add(actorNode.Actor.GetScript(ElementType.Type)); + } + } + Editor.SetValue(list); + } + } + + _dragHandlers.OnDragDrop(null); + } + + return result; + } + } } } 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/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index 9607680f2..c215a5ab7 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -72,14 +72,14 @@ namespace FlaxEditor.CustomEditors.Editors return; _isRefreshing = true; var slots = _modelInstance.MaterialSlots; - var material = _materialEditor.Picker.SelectedAsset as MaterialBase; + var material = _materialEditor.Picker.Validator.SelectedAsset as MaterialBase; var defaultMaterial = GPUDevice.Instance.DefaultMaterial; var value = (ModelInstanceEntry)Values[0]; var prevMaterial = value.Material; if (!material) { // Fallback to default material - _materialEditor.Picker.SelectedAsset = defaultMaterial; + _materialEditor.Picker.Validator.SelectedAsset = defaultMaterial; value.Material = defaultMaterial; } else if (material == slots[_entryIndex].Material) diff --git a/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs b/Source/Editor/CustomEditors/Editors/ObjectSwitcherEditor.cs index 2ef993e75..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; 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/Elements/DoubleValueElement.cs b/Source/Editor/CustomEditors/Elements/DoubleValueElement.cs index e46040a42..07af5e991 100644 --- a/Source/Editor/CustomEditors/Elements/DoubleValueElement.cs +++ b/Source/Editor/CustomEditors/Elements/DoubleValueElement.cs @@ -22,7 +22,7 @@ namespace FlaxEditor.CustomEditors.Elements /// /// [Deprecated on 26.05.2022, expires on 26.05.2024] /// - [System.Obsolete("Deprecated in 1.4")] + [System.Obsolete("Deprecated in 1.4, use ValueBox instead")] public DoubleValueBox DoubleValue => ValueBox; /// diff --git a/Source/Editor/CustomEditors/Elements/FloatValueElement.cs b/Source/Editor/CustomEditors/Elements/FloatValueElement.cs index 552e9d125..789d8966e 100644 --- a/Source/Editor/CustomEditors/Elements/FloatValueElement.cs +++ b/Source/Editor/CustomEditors/Elements/FloatValueElement.cs @@ -22,7 +22,7 @@ namespace FlaxEditor.CustomEditors.Elements /// /// [Deprecated on 26.05.2022, expires on 26.05.2024] /// - [System.Obsolete("Deprecated in 1.4, ValueBox instead")] + [System.Obsolete("Deprecated in 1.4, use ValueBox instead")] public FloatValueBox FloatValue => ValueBox; /// 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 e0c460d0c..b384b6515 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; @@ -365,7 +364,7 @@ namespace FlaxEditor { foreach (var preview in activePreviews) { - if (preview == loadingPreview || + if (preview == loadingPreview || (preview.Instance != null && (preview.Instance == control || preview.Instance.HasActorInHierarchy(control)))) { // Link it to the prefab preview to see it in the editor @@ -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,6 +931,11 @@ namespace FlaxEditor /// The . /// Animation = 11, + + /// + /// The . + /// + BehaviorTree = 12, } /// diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index 3e5d22eb0..1476832c4 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -5,6 +5,7 @@ using System.IO; using FlaxEditor.Content; using FlaxEditor.GUI.Drag; using FlaxEditor.Scripting; +using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; @@ -17,189 +18,21 @@ namespace FlaxEditor.GUI /// /// [HideInEditor] - public class AssetPicker : Control, IContentItemOwner + public class AssetPicker : Control { private const float DefaultIconSize = 64; private const float ButtonsOffset = 2; private const float ButtonsSize = 12; - private Asset _selected; - private ContentItem _selectedItem; - private ScriptType _type; - private string _fileExtension; - private bool _isMouseDown; private Float2 _mouseDownPos; private Float2 _mousePos; private DragItems _dragOverElement; /// - /// Gets or sets the selected item. + /// The asset validator. Used to ensure only appropriate items can be picked. /// - public ContentItem SelectedItem - { - get => _selectedItem; - set - { - if (_selectedItem == value) - return; - if (value == null) - { - if (_selected == null && _selectedItem is SceneItem) - { - // Deselect scene reference - _selectedItem.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - return; - } - - // Deselect - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; - OnSelectedItemChanged(); - } - else if (value is SceneItem item) - { - if (_selectedItem == item) - return; - if (!IsValid(item)) - item = null; - - // Change value to scene reference (cannot load asset because scene can be already loaded - duplicated ID issue) - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = null; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - else if (value is AssetItem assetItem) - { - SelectedAsset = FlaxEngine.Content.LoadAsync(assetItem.ID); - } - else - { - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = value; - _selected = null; - OnSelectedItemChanged(); - } - } - } - - /// - /// Gets or sets the selected asset identifier. - /// - public Guid SelectedID - { - get - { - if (_selected != null) - return _selected.ID; - if (_selectedItem is AssetItem assetItem) - return assetItem.ID; - return Guid.Empty; - } - set => SelectedItem = Editor.Instance.ContentDatabase.FindAsset(value); - } - - /// - /// Gets or sets the selected content item path. - /// - 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); - } - } - } - - /// - /// Gets or sets the selected asset object. - /// - public Asset SelectedAsset - { - get => _selected; - set - { - // Check if value won't change - if (value == _selected) - return; - - // Find item from content database and check it - var item = value ? Editor.Instance.ContentDatabase.FindAsset(value.ID) : null; - if (item != null && !IsValid(item)) - item = null; - - // Change value - _selectedItem?.RemoveReference(this); - _selectedItem = item; - _selected = value; - _selectedItem?.AddReference(this); - OnSelectedItemChanged(); - } - } - - /// - /// Gets or sets the assets types that this picker accepts (it supports types derived from the given type). Use for generic file picker. - /// - public ScriptType AssetType - { - get => _type; - set - { - if (_type != value) - { - _type = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } - - /// - /// Gets or sets the content items extensions filter. Null if unused. - /// - public string FileExtension - { - get => _fileExtension; - set - { - if (_fileExtension != value) - { - _fileExtension = value; - - // Auto deselect if the current value is invalid - if (_selectedItem != null && !IsValid(_selectedItem)) - SelectedItem = null; - } - } - } + public AssetPickerValidator Validator { get; } /// /// Occurs when selected item gets changed. @@ -216,38 +49,6 @@ namespace FlaxEditor.GUI /// public bool CanEdit = true; - private bool IsValid(ContentItem item) - { - if (_fileExtension != null && !item.Path.EndsWith(_fileExtension)) - return false; - if (CheckValid != null && !CheckValid(item)) - return false; - if (_type == ScriptType.Null) - return true; - - if (item is AssetItem assetItem) - { - // Faster path for binary items (in-built) - if (assetItem is BinaryAssetItem binaryItem) - return _type.IsAssignableFrom(new ScriptType(binaryItem.Type)); - - // Type filter - var type = TypeUtils.GetType(assetItem.TypeName); - if (_type.IsAssignableFrom(type)) - return true; - - // Json assets can contain any type of the object defined by the C# type (data oriented design) - if (assetItem is JsonAssetItem && (_type.Type == typeof(JsonAsset) || _type.Type == typeof(Asset))) - return true; - - // Special case for scene asset references - if (_type.Type == typeof(SceneReference) && assetItem is SceneItem) - return true; - } - - return false; - } - /// /// Initializes a new instance of the class. /// @@ -264,7 +65,8 @@ namespace FlaxEditor.GUI public AssetPicker(ScriptType assetType, Float2 location) : base(location, new Float2(DefaultIconSize + ButtonsOffset + ButtonsSize, DefaultIconSize)) { - _type = assetType; + Validator = new AssetPickerValidator(assetType); + Validator.SelectedItemChanged += OnSelectedItemChanged; _mousePos = Float2.Minimum; } @@ -275,10 +77,10 @@ namespace FlaxEditor.GUI { // Update tooltip string tooltip; - if (_selectedItem is AssetItem assetItem) + if (Validator.SelectedItem is AssetItem assetItem) tooltip = assetItem.NamePath; else - tooltip = SelectedPath; + tooltip = Validator.SelectedPath; TooltipText = tooltip; SelectedItemChanged?.Invoke(); @@ -289,37 +91,13 @@ namespace FlaxEditor.GUI // Do the drag drop operation if has selected element if (new Rectangle(Float2.Zero, Size).Contains(ref _mouseDownPos)) { - if (_selected != null) - DoDragDrop(DragAssets.GetDragData(_selected)); - else if (_selectedItem != null) - DoDragDrop(DragItems.GetDragData(_selectedItem)); + if (Validator.SelectedAsset != null) + DoDragDrop(DragAssets.GetDragData(Validator.SelectedAsset)); + else if (Validator.SelectedItem != null) + DoDragDrop(DragItems.GetDragData(Validator.SelectedItem)); } } - /// - public void OnItemDeleted(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - - /// - public void OnItemRenamed(ContentItem item) - { - } - - /// - public void OnItemReimported(ContentItem item) - { - } - - /// - public void OnItemDispose(ContentItem item) - { - // Deselect item - SelectedItem = null; - } - private Rectangle IconRect => new Rectangle(0, 0, Height, Height); private Rectangle Button1Rect => new Rectangle(Height + ButtonsOffset, 0, ButtonsSize, ButtonsSize); @@ -341,10 +119,10 @@ namespace FlaxEditor.GUI if (CanEdit) Render2D.DrawSprite(style.ArrowDown, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { // Draw item preview - _selectedItem.DrawThumbnail(ref iconRect); + Validator.SelectedItem.DrawThumbnail(ref iconRect); // Draw buttons if (CanEdit) @@ -363,7 +141,7 @@ namespace FlaxEditor.GUI { Render2D.DrawText( style.FontSmall, - _selectedItem.ShortName, + Validator.SelectedItem.ShortName, new Rectangle(button1Rect.Right + 2, 0, sizeForTextLeft, ButtonsSize), style.Foreground, TextAlignment.Near, @@ -371,7 +149,7 @@ namespace FlaxEditor.GUI } } // Check if has no item but has an asset (eg. virtual asset) - else if (_selected) + else if (Validator.SelectedAsset) { // Draw remove button Render2D.DrawSprite(style.Cross, button3Rect, button3Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey); @@ -380,8 +158,8 @@ namespace FlaxEditor.GUI float sizeForTextLeft = Width - button1Rect.Right; if (sizeForTextLeft > 30) { - var name = _selected.GetType().Name; - if (_selected.IsVirtual) + var name = Validator.SelectedAsset.GetType().Name; + if (Validator.SelectedAsset.IsVirtual) name += " (virtual)"; Render2D.DrawText( style.FontSmall, @@ -407,9 +185,7 @@ namespace FlaxEditor.GUI /// public override void OnDestroy() { - _selectedItem?.RemoveReference(this); - _selectedItem = null; - _selected = null; + Validator.OnDestroy(); base.OnDestroy(); } @@ -463,57 +239,57 @@ namespace FlaxEditor.GUI // Buttons logic if (!CanEdit) { - if (Button1Rect.Contains(location) && _selectedItem != null) + if (Button1Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } } else if (Button1Rect.Contains(location)) { Focus(); - if (_type != ScriptType.Null) + if (Validator.AssetType != ScriptType.Null) { // Show asset picker popup - var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = AssetSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selected != null) - { - var selectedAssetName = Path.GetFileNameWithoutExtension(_selected.Path); + if (Validator.SelectedAsset != null) + { + var selectedAssetName = Path.GetFileNameWithoutExtension(Validator.SelectedAsset.Path); popup.ScrollToAndHighlightItemByName(selectedAssetName); } } else { // Show content item picker popup - var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, IsValid, item => + var popup = ContentSearchPopup.Show(this, Button1Rect.BottomLeft, Validator.IsValid, item => { - SelectedItem = item; + Validator.SelectedItem = item; RootWindow.Focus(); Focus(); }); - if (_selectedItem != null) + if (Validator.SelectedItem != null) { - popup.ScrollToAndHighlightItemByName(_selectedItem.ShortName); + popup.ScrollToAndHighlightItemByName(Validator.SelectedItem.ShortName); } } } - else if (_selected != null || _selectedItem != null) + else if (Validator.SelectedAsset != null || Validator.SelectedItem != null) { - if (Button2Rect.Contains(location) && _selectedItem != null) + if (Button2Rect.Contains(location) && Validator.SelectedItem != null) { // Select asset - Editor.Instance.Windows.ContentWin.Select(_selectedItem); + Editor.Instance.Windows.ContentWin.Select(Validator.SelectedItem); } else if (Button3Rect.Contains(location)) { // Deselect asset Focus(); - SelectedItem = null; + Validator.SelectedItem = null; } } } @@ -540,10 +316,10 @@ namespace FlaxEditor.GUI { Focus(); - if (_selectedItem != null && IconRect.Contains(location)) + if (Validator.SelectedItem != null && IconRect.Contains(location)) { // Open it - Editor.Instance.ContentEditing.Open(_selectedItem); + Editor.Instance.ContentEditing.Open(Validator.SelectedItem); } // Handled @@ -557,7 +333,7 @@ namespace FlaxEditor.GUI // Check if drop asset if (_dragOverElement == null) - _dragOverElement = new DragItems(IsValid); + _dragOverElement = new DragItems(Validator.IsValid); if (CanEdit && _dragOverElement.OnDragEnter(data)) { } @@ -590,7 +366,7 @@ namespace FlaxEditor.GUI if (CanEdit && _dragOverElement.HasValidDrag) { // Select element - SelectedItem = _dragOverElement.Objects[0]; + Validator.SelectedItem = _dragOverElement.Objects[0]; } // Clear cache diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs index 7af36fae0..49a60a04e 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs @@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.ContextMenu // Hide parent CM popups and set itself as child parentContextMenu.ShowChild(ContextMenu, PointToParent(ParentContextMenu, new Float2(Width, 0))); } - + /// public override bool OnMouseUp(Float2 location, MouseButton button) { diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 161b3f4ae..27878a763 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -319,7 +319,9 @@ namespace FlaxEditor.GUI.Dialogs protected override void OnShow() { // Auto cancel on lost focus +#if !PLATFORM_LINUX ((WindowRootControl)Root).Window.LostFocus += OnCancel; +#endif base.OnShow(); } diff --git a/Source/Editor/GUI/Dialogs/Dialog.cs b/Source/Editor/GUI/Dialogs/Dialog.cs index 07fc3ff0d..86e24e3b3 100644 --- a/Source/Editor/GUI/Dialogs/Dialog.cs +++ b/Source/Editor/GUI/Dialogs/Dialog.cs @@ -39,6 +39,11 @@ namespace FlaxEditor.GUI.Dialogs /// public DialogResult Result => _result; + /// + /// Returns the size of the dialog. + /// + public Float2 DialogSize => _dialogSize; + /// /// Initializes a new instance of the class. /// @@ -293,7 +298,7 @@ namespace FlaxEditor.GUI.Dialogs if (Root != null) { bool shiftDown = Root.GetKey(KeyboardKeys.Shift); - Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); + Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); } return true; } diff --git a/Source/Editor/GUI/Docking/DockHintWindow.cs b/Source/Editor/GUI/Docking/DockHintWindow.cs index 52c5dcd3c..6e2353441 100644 --- a/Source/Editor/GUI/Docking/DockHintWindow.cs +++ b/Source/Editor/GUI/Docking/DockHintWindow.cs @@ -44,11 +44,11 @@ namespace FlaxEditor.GUI.Docking var mousePos = window.MousePosition; var previousSize = window.Size; window.Restore(); - window.Position = FlaxEngine.Input.MouseScreenPosition - mousePos * window.Size / previousSize; + window.Position = Platform.MousePosition - mousePos * window.Size / previousSize; } // Calculate dragging offset and move window to the destination position - var mouseScreenPosition = FlaxEngine.Input.MouseScreenPosition; + var mouseScreenPosition = Platform.MousePosition; // If the _toMove window was not focused when initializing this window, the result vector only contains zeros // and to prevent a failure, we need to perform an update for the drag offset at later time which will be done in the OnMouseMove event handler. @@ -83,6 +83,7 @@ namespace FlaxEditor.GUI.Docking // Enable hit window presentation Proxy.Window.RenderingEnabled = true; Proxy.Window.Show(); + Proxy.Window.Focus(); } /// @@ -113,7 +114,7 @@ namespace FlaxEditor.GUI.Docking var window = _toMove.Window?.Window; if (window == null) return; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; // Move base window window.Position = mouse - _dragOffset; @@ -193,7 +194,7 @@ namespace FlaxEditor.GUI.Docking // Move window to the mouse position (with some offset for caption bar) var window = (WindowRootControl)toMove.Root; - var mouse = FlaxEngine.Input.MouseScreenPosition; + var mouse = Platform.MousePosition; window.Window.Position = mouse - new Float2(8, 8); // Get floating panel @@ -244,7 +245,7 @@ namespace FlaxEditor.GUI.Docking private void UpdateRects() { // Cache mouse position - _mouse = FlaxEngine.Input.MouseScreenPosition; + _mouse = Platform.MousePosition; // Check intersection with any dock panel var uiMouse = _mouse; @@ -270,15 +271,16 @@ namespace FlaxEditor.GUI.Docking // Cache dock rectangles var size = _rectDock.Size; var offset = _rectDock.Location; - float BorderMargin = 4.0f; - float ProxyHintWindowsSize2 = Proxy.HintWindowsSize * 0.5f; - float centerX = size.X * 0.5f; - float centerY = size.Y * 0.5f; - _rUpper = new Rectangle(centerX - ProxyHintWindowsSize2, BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rBottom = new Rectangle(centerX - ProxyHintWindowsSize2, size.Y - Proxy.HintWindowsSize - BorderMargin, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rLeft = new Rectangle(BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rRight = new Rectangle(size.X - Proxy.HintWindowsSize - BorderMargin, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; - _rCenter = new Rectangle(centerX - ProxyHintWindowsSize2, centerY - ProxyHintWindowsSize2, Proxy.HintWindowsSize, Proxy.HintWindowsSize) + offset; + var borderMargin = 4.0f; + var hintWindowsSize = Proxy.HintWindowsSize * Platform.DpiScale; + var hintWindowsSize2 = hintWindowsSize * 0.5f; + var centerX = size.X * 0.5f; + var centerY = size.Y * 0.5f; + _rUpper = new Rectangle(centerX - hintWindowsSize2, borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rBottom = new Rectangle(centerX - hintWindowsSize2, size.Y - hintWindowsSize - borderMargin, hintWindowsSize, hintWindowsSize) + offset; + _rLeft = new Rectangle(borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rRight = new Rectangle(size.X - hintWindowsSize - borderMargin, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; + _rCenter = new Rectangle(centerX - hintWindowsSize2, centerY - hintWindowsSize2, hintWindowsSize, hintWindowsSize) + offset; // Hit test DockState toSet = DockState.Float; @@ -428,7 +430,6 @@ namespace FlaxEditor.GUI.Docking { if (Window == null) { - // Create proxy window var settings = CreateWindowSettings.Default; settings.Title = "DockHint.Window"; settings.Size = initSize; @@ -440,12 +441,10 @@ namespace FlaxEditor.GUI.Docking settings.IsRegularWindow = false; settings.SupportsTransparency = true; settings.ShowInTaskbar = false; - settings.ShowAfterFirstPaint = true; + settings.ShowAfterFirstPaint = false; settings.IsTopmost = true; Window = Platform.CreateWindow(ref settings); - - // Set opacity and background color Window.Opacity = 0.6f; Window.GUI.BackgroundColor = Style.Current.DragWindow; } @@ -465,7 +464,7 @@ namespace FlaxEditor.GUI.Docking var settings = CreateWindowSettings.Default; settings.Title = name; - settings.Size = new Float2(HintWindowsSize); + settings.Size = new Float2(HintWindowsSize * Platform.DpiScale); settings.AllowInput = false; settings.AllowMaximize = false; settings.AllowMinimize = false; @@ -479,7 +478,6 @@ namespace FlaxEditor.GUI.Docking settings.ShowAfterFirstPaint = false; win = Platform.CreateWindow(ref settings); - win.Opacity = 0.6f; win.GUI.BackgroundColor = Style.Current.DragWindow; } diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index e544285a5..b04aad08c 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -465,36 +465,47 @@ namespace FlaxEditor.GUI.Docking { if (Parent.Parent is SplitPanel splitter) { - // Check if has any child panels - var childPanel = new List(_childPanels); - for (int i = 0; i < childPanel.Count; i++) + // Check if there is another nested dock panel inside this dock panel and extract it here + var childPanels = _childPanels.ToArray(); + if (childPanels.Length != 0) { - // Undock all tabs - var panel = childPanel[i]; - int count = panel.TabsCount; - while (count-- > 0) + // Move tabs from child panels into this one + DockWindow selectedTab = null; + foreach (var childPanel in childPanels) { - panel.GetTab(0).Close(); + var childPanelTabs = childPanel.Tabs.ToArray(); + for (var i = 0; i < childPanelTabs.Length; i++) + { + var childPanelTab = childPanelTabs[i]; + if (selectedTab == null && childPanelTab.IsSelected) + selectedTab = childPanelTab; + childPanel.UndockWindow(childPanelTab); + AddTab(childPanelTab, false); + } } + if (selectedTab != null) + SelectTab(selectedTab); } - - // Unlink splitter - var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); - splitter.Parent = null; - - // Move controls from second split panel to the split panel parent - var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; - var srcPanelChildrenCount = scrPanel.ChildrenCount; - for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + else { - scrPanel.GetChild(i).Parent = splitterParent; - } - Assert.IsTrue(scrPanel.ChildrenCount == 0); - Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + // Unlink splitter + var splitterParent = splitter.Parent; + Assert.IsNotNull(splitterParent); + splitter.Parent = null; - // Delete - splitter.Dispose(); + // Move controls from second split panel to the split panel parent + var scrPanel = Parent == splitter.Panel2 ? splitter.Panel1 : splitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + // Delete + splitter.Dispose(); + } } else if (!IsMaster) { @@ -582,19 +593,17 @@ namespace FlaxEditor.GUI.Docking /// Adds the tab. /// /// The window to insert as a tab. - protected virtual void AddTab(DockWindow window) + /// True if auto-select newly added tab. + protected virtual void AddTab(DockWindow window, bool autoSelect = true) { - // Dock _tabs.Add(window); window.ParentDockPanel = this; - - // Select tab - SelectTab(window); + if (autoSelect) + SelectTab(window); } private void CreateTabsProxy() { - // Check if has no tabs proxy created if (_tabsProxy == null) { // Create proxy and make set simple full dock diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f8ffe62ad..7bb85751a 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -72,7 +72,7 @@ namespace FlaxEditor.GUI.Docking settings.Size = size; settings.Position = location; settings.MinimumSize = new Float2(1); - settings.MaximumSize = new Float2(4096); + settings.MaximumSize = Float2.Zero; // Unlimited size settings.Fullscreen = false; settings.HasBorder = true; settings.SupportsTransparency = 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