From a570d6d1781755917d986b901f6f12984b5eac80 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 20 Dec 2022 21:09:51 +0100 Subject: [PATCH] Refactor **Actor tags into hierarchical reusable Tags** system for better gameplay scripting --- .../CustomEditors/Editors/ActorTagEditor.cs | 57 -- .../Editors/CultureInfoEditor.cs | 28 +- .../Editor/CustomEditors/Editors/TagEditor.cs | 497 ++++++++++++++++++ Source/Editor/GUI/Popups/RenamePopup.cs | 5 + Source/Editor/GUI/Tree/TreeNode.cs | 25 +- Source/Editor/GUI/Tree/TreeNodeWithAddons.cs | 121 +++++ .../Editor/Managed/ManagedEditor.Internal.cpp | 10 +- Source/Editor/Modules/SceneEditingModule.cs | 2 +- Source/Editor/Utilities/Utils.cs | 34 +- .../Core/Config/LayersAndTagsSettings.cs | 7 - Source/Engine/Level/Actor.cpp | 103 ++-- Source/Engine/Level/Actor.h | 60 +-- Source/Engine/Level/Level.cpp | 15 +- Source/Engine/Level/Level.h | 12 - Source/Engine/Level/Tags.cpp | 44 ++ Source/Engine/Level/Tags.cs | 116 ++++ Source/Engine/Level/Tags.h | 88 ++++ Source/flax.natvis | 6 + 18 files changed, 991 insertions(+), 239 deletions(-) delete mode 100644 Source/Editor/CustomEditors/Editors/ActorTagEditor.cs create mode 100644 Source/Editor/CustomEditors/Editors/TagEditor.cs create mode 100644 Source/Editor/GUI/Tree/TreeNodeWithAddons.cs create mode 100644 Source/Engine/Level/Tags.cpp create mode 100644 Source/Engine/Level/Tags.cs create mode 100644 Source/Engine/Level/Tags.h diff --git a/Source/Editor/CustomEditors/Editors/ActorTagEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTagEditor.cs deleted file mode 100644 index d274e96ee..000000000 --- a/Source/Editor/CustomEditors/Editors/ActorTagEditor.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. - -using FlaxEditor.Content.Settings; -using FlaxEditor.CustomEditors.Elements; -using FlaxEditor.GUI; - -namespace FlaxEditor.CustomEditors.Editors -{ - /// - /// Custom editor for picking actor tag. Instead of choosing tag index or entering tag text it shows a combo box with simple tag picking by name. - /// - public sealed class ActorTagEditor : CustomEditor - { - private ComboBoxElement element; - private const string NoTagText = "Untagged"; - - /// - public override DisplayStyle Style => DisplayStyle.Inline; - - /// - public override void Initialize(LayoutElementsContainer layout) - { - element = layout.ComboBox(); - element.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged; - - // Set tag names - element.ComboBox.AddItem(NoTagText); - element.ComboBox.AddItems(LayersAndTagsSettings.GetCurrentTags()); - } - - private void OnSelectedIndexChanged(ComboBox comboBox) - { - string value = comboBox.SelectedItem; - if (value == NoTagText) - value = string.Empty; - SetValue(value); - } - - /// - public override void Refresh() - { - base.Refresh(); - - if (HasDifferentValues) - { - // TODO: support different values on many actor selected - } - else - { - string value = (string)Values[0]; - if (string.IsNullOrEmpty(value)) - value = NoTagText; - element.ComboBox.SelectedItem = value; - } - } - } -} diff --git a/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs b/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs index 157c3af17..6b6fa943a 100644 --- a/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CultureInfoEditor.cs @@ -32,6 +32,7 @@ namespace FlaxEditor.CustomEditors.Editors { Width = 16.0f, Text = "...", + TooltipText = "Edit...", Parent = _label, }; button.SetAnchorPreset(AnchorPresets.MiddleRight, false, true); @@ -81,30 +82,6 @@ namespace FlaxEditor.CustomEditors.Editors } } - private static void UpdateFilter(TreeNode node, string filterText) - { - // Update children - bool isAnyChildVisible = false; - for (int i = 0; i < node.Children.Count; i++) - { - if (node.Children[i] is TreeNode child) - { - UpdateFilter(child, filterText); - isAnyChildVisible |= child.Visible; - } - } - - // Update itself - bool noFilter = string.IsNullOrWhiteSpace(filterText); - bool isThisVisible = noFilter || QueryFilterHelper.Match(filterText, node.Text); - bool isExpanded = isAnyChildVisible; - if (isExpanded) - node.Expand(true); - else - node.Collapse(true); - node.Visible = isThisVisible | isAnyChildVisible; - } - private void ShowPicker() { var menu = CreatePicker(Culture, value => { Culture = value; }); @@ -147,8 +124,7 @@ namespace FlaxEditor.CustomEditors.Editors if (tree.IsLayoutLocked) return; root.LockChildrenRecursive(); - var query = searchBox.Text; - UpdateFilter(root, query); + Utilities.Utils.UpdateSearchPopupFilter(root, searchBox.Text); root.UnlockChildrenRecursive(); menu.PerformLayout(); }; diff --git a/Source/Editor/CustomEditors/Editors/TagEditor.cs b/Source/Editor/CustomEditors/Editors/TagEditor.cs new file mode 100644 index 000000000..c5c1ee6b0 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/TagEditor.cs @@ -0,0 +1,497 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using FlaxEditor.GUI; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Tree; +using FlaxEngine; +using FlaxEngine.GUI; +using System.Collections.Generic; +using System; +using System.Linq; +using System.Text; +using FlaxEditor.Content.Settings; + +namespace FlaxEditor.CustomEditors.Editors +{ + /// + /// Custom editor for . + /// + [CustomEditor(typeof(Tag)), DefaultEditor] + public sealed class TagEditor : CustomEditor + { + private ClickableLabel _label; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + _label = layout.ClickableLabel(Tag.ToString()).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 = Tag.ToString(); + } + + private Tag Tag + { + get + { + if (Values[0] is Tag asTag) + return asTag; + if (Values[0] is int asInt) + return new Tag(asInt); + if (Values[0] is string asString) + return Tags.Get(asString); + return Tag.Default; + } + set + { + if (Values[0] is Tag) + SetValue(value); + if (Values[0] is int) + SetValue(value.Index); + else if (Values[0] is string) + SetValue(value.ToString()); + } + } + + private void ShowPicker() + { + var menu = CreatePicker(Tag, null, new PickerData + { + IsSingle = true, + SetValue = value => { Tag = value; }, + }); + menu.Show(_label, new Float2(0, _label.Height)); + } + + internal class PickerData + { + public bool IsEditing; + public bool IsSingle; + public Action SetValue; + public Action SetValues; + public List CachedTags; + } + + private static void UncheckAll(TreeNode n, TreeNode except = null) + { + foreach (var child in n.Children) + { + if (child is CheckBox checkBox && except != n) + checkBox.Checked = false; + else if (child is TreeNode treeNode) + UncheckAll(treeNode, except); + } + } + + private static void UncheckAll(Tree n, TreeNode except = null) + { + foreach (var child in n.Children) + { + if (child is TreeNode treeNode) + UncheckAll(treeNode, except); + } + } + + private static void GetTags(TreeNode n, PickerData pickerData) + { + if (n is TreeNodeWithAddons a && a.Addons.Count != 0 && a.Addons[0] is CheckBox c && c.Checked) + pickerData.CachedTags.Add(new Tag((int)n.Tag)); + foreach (var child in n.Children) + { + if (child is TreeNode treeNode) + GetTags(treeNode, pickerData); + } + } + + private static void GetTags(Tree n, PickerData pickerData) + { + foreach (var child in n.Children) + { + if (child is TreeNode treeNode) + GetTags(treeNode, pickerData); + } + } + + private static void OnCheckboxEdited(TreeNode node, CheckBox c, PickerData pickerData) + { + if (pickerData.IsEditing) + return; + pickerData.IsEditing = true; + if (pickerData.IsSingle) + { + UncheckAll(node.ParentTree, node); + var value = new Tag(c.Checked ? (int)node.Tag : -1); + pickerData.SetValue?.Invoke(value); + pickerData.SetValues?.Invoke(new[] { value }); + } + else + { + if (pickerData.CachedTags == null) + pickerData.CachedTags = new List(); + else + pickerData.CachedTags.Clear(); + GetTags(node.ParentTree, pickerData); + pickerData.SetValue?.Invoke(pickerData.CachedTags.Count != 0 ? pickerData.CachedTags[0] : Tag.Default); + pickerData.SetValues?.Invoke(pickerData.CachedTags.ToArray()); + } + pickerData.IsEditing = false; + } + + + private static void OnAddButtonClicked(Tree tree, TreeNode parentNode, PickerData pickerData) + { + // Create new node + var nodeIndent = 16.0f; + var indentation = 0; + var parentTag = string.Empty; + if (parentNode.CustomArrowRect.HasValue) + { + indentation = (int)((parentNode.CustomArrowRect.Value.Location.X - 18) / nodeIndent) + 1; + var parentIndex = (int)parentNode.Tag; + parentTag = Tags.List[parentIndex]; + } + var node = new TreeNodeWithAddons + { + ChildrenIndent = nodeIndent, + CullChildren = false, + ClipChildren = false, + TextMargin = new Margin(22.0f, 2.0f, 2.0f, 2.0f), + CustomArrowRect = new Rectangle(18 + indentation * nodeIndent, 2, 12, 12), + BackgroundColorSelected = Color.Transparent, + BackgroundColorHighlighted = parentNode.BackgroundColorHighlighted, + }; + var checkbox = new CheckBox(32.0f + indentation * nodeIndent, 0) + { + Height = 16.0f, + IsScrollable = false, + Parent = node, + }; + node.Addons.Add(checkbox); + checkbox.StateChanged += c => OnCheckboxEdited(node, c, pickerData); + var addButton = new Button(tree.Width - 16, 0, 14, 14) + { + Text = "+", + TooltipText = "Add subtag within this tag namespace", + IsScrollable = false, + BorderColor = Color.Transparent, + BackgroundColor = Color.Transparent, + Parent = node, + AnchorPreset = AnchorPresets.TopRight, + }; + node.Addons.Add(addButton); + addButton.ButtonClicked += button => OnAddButtonClicked(tree, node, pickerData); + + // Link + node.Parent = parentNode; + node.IndexInParent = 0; + parentNode.Expand(true); + ((Panel)tree.Parent.Parent).ScrollViewTo(node); + + // Start renaming the tag + var prefix = parentTag.Length != 0 ? parentTag + '.' : string.Empty; + var renameArea = node.HeaderRect; + if (renameArea.Location.X < nodeIndent) + { + // Fix root node's child renaming + renameArea.Location.X += nodeIndent; + renameArea.Size.X -= nodeIndent; + } + var dialog = RenamePopup.Show(node, renameArea, prefix, false); + var cursor = dialog.InputField.TextLength; + dialog.InputField.SelectionRange = new TextRange(cursor, cursor); + dialog.Validate = (popup, value) => + { + // Ensure that name is unique + if (Tags.List.Contains(value)) + return false; + + // Ensure user entered direct subtag of the parent node + if (value.StartsWith(popup.InitialValue)) + { + var name = value.Substring(popup.InitialValue.Length); + if (name.Length > 0 && name.IndexOf('.') == -1) + return true; + } + return false; + }; + dialog.Renamed += popup => + { + // Get tag name + var tagName = popup.Text; + var name = tagName.Substring(popup.InitialValue.Length); + if (name.Length == 0) + return; + + // Add tag + var tag = Tags.Get(tagName); + node.Text = name; + node.Tag = tag.Index; + var settingsAsset = GameSettings.LoadAsset(); + if (settingsAsset && !settingsAsset.WaitForLoaded()) + { + // Save any local changes to the tags asset in Editor + var settingsAssetItem = Editor.Instance.ContentDatabase.FindAsset(settingsAsset.ID) as Content.JsonAssetItem; + var assetWindow = Editor.Instance.Windows.FindEditor(settingsAssetItem) as Windows.Assets.JsonAssetWindow; + assetWindow?.Save(); + + // Update asset + var settingsObj = (LayersAndTagsSettings)settingsAsset.Instance; + settingsObj.Tags.Add(tagName); + settingsAsset.SetInstance(settingsObj); + } + }; + dialog.Closed += popup => + { + // Remove temporary node if renaming was canceled + if (popup.InitialValue == popup.Text || popup.Text.Length == 0) + node.Dispose(); + }; + } + + internal static ContextMenuBase CreatePicker(Tag value, Tag[] values, PickerData pickerData) + { + // Initialize search popup + var menu = Utilities.Utils.CreateSearchPopup(out var searchBox, out var tree, 20.0f); + + // Create tree with tags hierarchy + tree.Margin = new Margin(-16.0f, 0.0f, -16.0f, -0.0f); // Hide root node + var tags = Tags.List; + var nameToNode = new Dictionary(); + var style = FlaxEngine.GUI.Style.Current; + var nodeBackgroundColorHighlighted = style.BackgroundHighlighted * 0.5f; + var nodeIndent = 16.0f; + var root = tree.AddChild(); + for (var i = 0; i < tags.Length; i++) + { + var tag = tags[i]; + bool isSelected = pickerData.IsSingle ? value.Index == i : values.Contains(new Tag(i)); + + // Count parent tags count + int indentation = 0; + for (int j = 0; j < tag.Length; j++) + { + if (tag[j] == '.') + indentation++; + } + + // Create node + var node = new TreeNodeWithAddons + { + Tag = i, + Text = tag, + ChildrenIndent = nodeIndent, + CullChildren = false, + ClipChildren = false, + TextMargin = new Margin(22.0f, 2.0f, 2.0f, 2.0f), + CustomArrowRect = new Rectangle(18 + indentation * nodeIndent, 2, 12, 12), + BackgroundColorSelected = Color.Transparent, + BackgroundColorHighlighted = nodeBackgroundColorHighlighted, + }; + var checkbox = new CheckBox(32.0f + indentation * nodeIndent, 0, isSelected) + { + Height = 16.0f, + IsScrollable = false, + Parent = node, + }; + node.Addons.Add(checkbox); + checkbox.StateChanged += c => OnCheckboxEdited(node, c, pickerData); + var addButton = new Button(menu.Width - 16, 0, 14, 14) + { + Text = "+", + TooltipText = "Add subtag within this tag namespace", + IsScrollable = false, + BorderColor = Color.Transparent, + BackgroundColor = Color.Transparent, + Parent = node, + AnchorPreset = AnchorPresets.TopRight, + }; + node.Addons.Add(addButton); + addButton.ButtonClicked += button => OnAddButtonClicked(tree, node, pickerData); + + // Link to parent + { + var lastDotIndex = tag.LastIndexOf('.'); + var parentTagName = lastDotIndex != -1 ? tag.Substring(0, lastDotIndex) : string.Empty; + if (!nameToNode.TryGetValue(parentTagName, out ContainerControl parent)) + parent = root; + node.Parent = parent; + } + nameToNode[tag] = node; + + // Expand selected nodes to be visible in hierarchy + if (isSelected) + node.ExpandAllParents(true); + } + nameToNode.Clear(); + + // Adds top panel with utility buttons + var buttonsPanel = new HorizontalPanel + { + Margin = new Margin(1.0f), + AutoSize = false, + Bounds = new Rectangle(0, 0, menu.Width, 20.0f), + Parent = menu, + }; + var buttonsSize = new Float2((menu.Width - buttonsPanel.Margin.Width) / 4.0f - buttonsPanel.Spacing, 18.0f); + var buttonExpandAll = new Button + { + Size = buttonsSize, + Parent = buttonsPanel, + Text = "Expand all", + }; + buttonExpandAll.Clicked += () => root.ExpandAll(true); + var buttonCollapseAll = new Button + { + Size = buttonsSize, + Parent = buttonsPanel, + Text = "Collapse all", + }; + buttonCollapseAll.Clicked += () => + { + root.CollapseAll(true); + root.Expand(true); + }; + var buttonAddTag = new Button + { + Size = buttonsSize, + Parent = buttonsPanel, + Text = "Add Tag", + }; + buttonAddTag.Clicked += () => OnAddButtonClicked(tree, root, pickerData); + var buttonReset = new Button + { + Size = buttonsSize, + Parent = buttonsPanel, + Text = "Reset", + }; + buttonReset.Clicked += () => + { + pickerData.IsEditing = true; + UncheckAll(root); + pickerData.IsEditing = false; + pickerData.SetValue?.Invoke(Tag.Default); + pickerData.SetValues?.Invoke(null); + }; + + // Setup search filter + searchBox.TextChanged += delegate + { + if (tree.IsLayoutLocked) + return; + root.LockChildrenRecursive(); + Utilities.Utils.UpdateSearchPopupFilter(root, searchBox.Text); + root.UnlockChildrenRecursive(); + menu.PerformLayout(); + }; + + // Prepare for display + root.SortChildrenRecursive(); + root.Expand(true); + + return menu; + } + } + + /// + /// Custom editor for array of . + /// + [CustomEditor(typeof(Tag[])), DefaultEditor] + public sealed class TagsEditor : CustomEditor + { + private ClickableLabel _label; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + _label = layout.ClickableLabel(GetText(out _)).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 = GetText(out var tags); + _label.Height = Math.Max(tags.Length, 1) * 18.0f; + } + + private string GetText(out Tag[] tags) + { + tags = Tags; + if (tags.Length == 0) + return string.Empty; + if (tags.Length == 1) + return tags[0].ToString(); + var sb = new StringBuilder(); + for (var i = 0; i < tags.Length; i++) + { + var tag = tags[i]; + if (i != 0) + sb.AppendLine(); + sb.Append(tag.ToString()); + } + return sb.ToString(); + } + + private Tag[] Tags + { + get + { + if (Values[0] is Tag[] asArray) + return asArray; + if (Values[0] is List asList) + return asList.ToArray(); + return Utils.GetEmptyArray(); + } + set + { + if (Values[0] is Tag[]) + SetValue(value); + if (Values[0] is List) + SetValue(new List(value)); + } + } + + private void ShowPicker() + { + var menu = TagEditor.CreatePicker(Tag.Default, Tags, new TagEditor.PickerData + { + SetValues = value => { Tags = value; }, + }); + menu.Show(_label, new Float2(0, _label.Height)); + } + } +} diff --git a/Source/Editor/GUI/Popups/RenamePopup.cs b/Source/Editor/GUI/Popups/RenamePopup.cs index 01fbdc03f..03689a114 100644 --- a/Source/Editor/GUI/Popups/RenamePopup.cs +++ b/Source/Editor/GUI/Popups/RenamePopup.cs @@ -57,6 +57,11 @@ namespace FlaxEditor.GUI set => _inputField.Text = value; } + /// + /// Gets the text input field control. + /// + public TextBox InputField => _inputField; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index 8f2cc1c13..e5d5c4355 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -217,7 +217,7 @@ namespace FlaxEditor.GUI.Tree /// /// Gets the arrow rectangle. /// - public Rectangle ArrowRect => new Rectangle(_xOffset + 2 + _margin.Left, 2, 12, 12); + public Rectangle ArrowRect => CustomArrowRect.HasValue ? CustomArrowRect.Value : new Rectangle(_xOffset + 2 + _margin.Left, 2, 12, 12); /// /// Gets the header rectangle. @@ -248,6 +248,12 @@ namespace FlaxEditor.GUI.Tree } } + /// + /// Custom arrow rectangle within node. + /// + [HideInEditor, NoSerialize] + public Rectangle? CustomArrowRect; + /// /// Gets the drag over action type. /// @@ -277,7 +283,7 @@ namespace FlaxEditor.GUI.Tree /// Initializes a new instance of the class. /// public TreeNode() - : this(false) + : this(false, SpriteHandle.Invalid, SpriteHandle.Invalid) { } @@ -486,11 +492,14 @@ namespace FlaxEditor.GUI.Tree /// True if event has been handled. protected virtual bool OnMouseDoubleClickHeader(ref Float2 location, MouseButton button) { - // Toggle open state - if (_opened) - Collapse(); - else - Expand(); + if (HasAnyVisibleChild) + { + // Toggle open state + if (_opened) + Collapse(); + else + Expand(); + } // Handled return true; @@ -754,7 +763,7 @@ namespace FlaxEditor.GUI.Tree } // Check if mouse hits arrow - if (HasAnyVisibleChild && _mouseOverArrow) + if (_mouseOverArrow && HasAnyVisibleChild) { // Toggle open state if (_opened) diff --git a/Source/Editor/GUI/Tree/TreeNodeWithAddons.cs b/Source/Editor/GUI/Tree/TreeNodeWithAddons.cs new file mode 100644 index 000000000..40e81ff40 --- /dev/null +++ b/Source/Editor/GUI/Tree/TreeNodeWithAddons.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using FlaxEngine; +using FlaxEngine.GUI; +using System.Collections.Generic; + +namespace FlaxEditor.GUI.Tree +{ + /// + /// Tree node control with in-built checkbox. + /// + [HideInEditor] + public class TreeNodeWithAddons : TreeNode + { + /// + /// The additional controls (eg. added to the header). + /// + public List Addons = new List(); + + /// + public override void Draw() + { + base.Draw(); + + foreach (var child in Addons) + { + Render2D.PushTransform(ref child._cachedTransform); + child.Draw(); + Render2D.PopTransform(); + } + } + + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + foreach (var child in Addons) + { + if (child.Visible && child.Enabled) + { + if (IntersectsChildContent(child, location, out var childLocation)) + { + if (child.OnMouseDown(childLocation, button)) + return true; + } + } + } + + return base.OnMouseDown(location, button); + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + foreach (var child in Addons) + { + if (child.Visible && child.Enabled) + { + if (IntersectsChildContent(child, location, out var childLocation)) + { + if (child.OnMouseUp(childLocation, button)) + return true; + } + } + } + + return base.OnMouseUp(location, button); + } + + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + foreach (var child in Addons) + { + if (child.Visible && child.Enabled) + { + if (IntersectsChildContent(child, location, out var childLocation)) + { + if (child.OnMouseDoubleClick(childLocation, button)) + return true; + } + } + } + + return base.OnMouseDoubleClick(location, button); + } + + /// + public override void OnMouseMove(Float2 location) + { + base.OnMouseMove(location); + + if (IsCollapsed) + { + foreach (var child in Addons) + { + if (child.Visible && child.Enabled) + { + if (IntersectsChildContent(child, location, out var childLocation)) + { + if (child.IsMouseOver) + { + // Move + child.OnMouseMove(childLocation); + } + else + { + // Enter + child.OnMouseEnter(childLocation); + } + } + else if (child.IsMouseOver) + { + // Leave + child.OnMouseLeave(); + } + } + } + } + } + } +} diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index 3d236cd49..7e81a7f58 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -327,13 +327,8 @@ namespace CustomEditorsUtilInternal } } -namespace LayersAndTagsSettingsInternal1 +namespace LayersAndTagsSettingsInternal { - MonoArray* GetCurrentTags() - { - return MUtils::ToArray(Level::Tags); - } - MonoArray* GetCurrentLayers() { return MUtils::ToArray(Span(Level::Layers, Math::Max(1, Level::GetNonEmptyLayerNamesCount()))); @@ -1125,8 +1120,7 @@ public: ADD_INTERNAL_CALL("FlaxEditor.Content.Import.TextureImportEntry::Internal_GetTextureImportOptions", &GetTextureImportOptions); ADD_INTERNAL_CALL("FlaxEditor.Content.Import.ModelImportEntry::Internal_GetModelImportOptions", &GetModelImportOptions); ADD_INTERNAL_CALL("FlaxEditor.Content.Import.AudioImportEntry::Internal_GetAudioImportOptions", &GetAudioImportOptions); - ADD_INTERNAL_CALL("FlaxEditor.Content.Settings.LayersAndTagsSettings::GetCurrentTags", &LayersAndTagsSettingsInternal1::GetCurrentTags); - ADD_INTERNAL_CALL("FlaxEditor.Content.Settings.LayersAndTagsSettings::GetCurrentLayers", &LayersAndTagsSettingsInternal1::GetCurrentLayers); + ADD_INTERNAL_CALL("FlaxEditor.Content.Settings.LayersAndTagsSettings::GetCurrentLayers", &LayersAndTagsSettingsInternal::GetCurrentLayers); ADD_INTERNAL_CALL("FlaxEditor.Content.Settings.GameSettings::Apply", &GameSettingsInternal1::Apply); ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_CloseSplashScreen", &CloseSplashScreen); ADD_INTERNAL_CALL("FlaxEditor.Editor::Internal_CreateAsset", &CreateAsset); diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs index 7b28367ff..8fa7001a1 100644 --- a/Source/Editor/Modules/SceneEditingModule.cs +++ b/Source/Editor/Modules/SceneEditingModule.cs @@ -379,7 +379,7 @@ namespace FlaxEditor.Modules actor.StaticFlags = old.StaticFlags; actor.HideFlags = old.HideFlags; actor.Layer = old.Layer; - actor.Tag = old.Tag; + actor.Tags = old.Tags; actor.Name = old.Name; actor.IsActive = old.IsActive; diff --git a/Source/Editor/Utilities/Utils.cs b/Source/Editor/Utilities/Utils.cs index 74ffb721b..c37d4c113 100644 --- a/Source/Editor/Utilities/Utils.cs +++ b/Source/Editor/Utilities/Utils.cs @@ -949,14 +949,15 @@ namespace FlaxEditor.Utilities /// /// The search box. /// The tree control. + /// Amount of additional space above the search box to put custom UI. /// The created menu to setup and show. - public static ContextMenuBase CreateSearchPopup(out TextBox searchBox, out Tree tree) + public static ContextMenuBase CreateSearchPopup(out TextBox searchBox, out Tree tree, float headerHeight = 0) { var menu = new ContextMenuBase { - Size = new Float2(320, 220), + Size = new Float2(320, 220 + headerHeight), }; - searchBox = new TextBox(false, 1, 1) + searchBox = new TextBox(false, 1, headerHeight + 1) { Width = menu.Width - 3, WatermarkText = "Search...", @@ -980,6 +981,33 @@ namespace FlaxEditor.Utilities return menu; } + /// + /// Updates (recursivly) search popup tree structures based on the filter text. + /// + public static void UpdateSearchPopupFilter(TreeNode node, string filterText) + { + // Update children + bool isAnyChildVisible = false; + for (int i = 0; i < node.Children.Count; i++) + { + if (node.Children[i] is TreeNode child) + { + UpdateSearchPopupFilter(child, filterText); + isAnyChildVisible |= child.Visible; + } + } + + // Update itself + bool noFilter = string.IsNullOrWhiteSpace(filterText); + bool isThisVisible = noFilter || QueryFilterHelper.Match(filterText, node.Text); + bool isExpanded = isAnyChildVisible; + if (isExpanded) + node.Expand(true); + else + node.Collapse(true); + node.Visible = isThisVisible | isAnyChildVisible; + } + /// /// Gets the asset name relative to the project root folder (without asset file extension) /// diff --git a/Source/Engine/Core/Config/LayersAndTagsSettings.cs b/Source/Engine/Core/Config/LayersAndTagsSettings.cs index 0396b7588..2f39d4595 100644 --- a/Source/Engine/Core/Config/LayersAndTagsSettings.cs +++ b/Source/Engine/Core/Config/LayersAndTagsSettings.cs @@ -20,13 +20,6 @@ namespace FlaxEditor.Content.Settings [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)] public string[] Layers = new string[32]; - /// - /// Gets the current tags collection. - /// - /// The tags collection. - [MethodImpl(MethodImplOptions.InternalCall)] - internal static extern string[] GetCurrentTags(); - /// /// Gets the current layer names (max 32 items but trims last empty items). /// diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index 1c4f4a2e1..d96bd005d 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -73,13 +73,12 @@ Actor::Actor(const SpawnParams& params) , _isPrefabRoot(false) , _isEnabled(false) , _layer(0) - , _tag(ACTOR_TAG_INVALID) - , _scene(nullptr) , _staticFlags(StaticFlags::FullyStatic) , _localTransform(Transform::Identity) , _transform(Transform::Identity) , _sphere(BoundingSphere::Empty) , _box(BoundingBox::Zero) + , _scene(nullptr) , _physicsScene(nullptr) , HideFlags(HideFlags::None) { @@ -439,23 +438,24 @@ void Actor::DestroyChildren(float timeLeft) } } -bool Actor::HasTag(const StringView& tag) const -{ - return HasTag() && tag == Level::Tags[_tag]; -} - const String& Actor::GetLayerName() const { return Level::Layers[_layer]; } -const String& Actor::GetTag() const +bool Actor::HasTag() const { - if (HasTag()) - { - return Level::Tags[_tag]; - } - return String::Empty; + return Tags.Count() != 0; +} + +bool Actor::HasTag(const Tag& tag) const +{ + return Tags.Contains(tag); +} + +bool Actor::HasTag(const StringView& tag) const +{ + return Tags.Contains(tag); } void Actor::SetLayer(int32 layerIndex) @@ -463,51 +463,10 @@ void Actor::SetLayer(int32 layerIndex) layerIndex = Math::Clamp(layerIndex, 0, 31); if (layerIndex == _layer) return; - _layer = layerIndex; OnLayerChanged(); } -void Actor::SetTagIndex(int32 tagIndex) -{ - if (tagIndex == ACTOR_TAG_INVALID) - { - } - else if (Level::Tags.IsEmpty()) - { - tagIndex = ACTOR_TAG_INVALID; - } - else - { - tagIndex = tagIndex < 0 ? ACTOR_TAG_INVALID : Math::Min(tagIndex, Level::Tags.Count() - 1); - } - if (tagIndex == _tag) - return; - - _tag = tagIndex; - OnTagChanged(); -} - -void Actor::SetTag(const StringView& tagName) -{ - int32 tagIndex; - if (tagName.IsEmpty()) - { - tagIndex = ACTOR_TAG_INVALID; - } - else - { - tagIndex = Level::Tags.Find(tagName); - if (tagIndex == -1) - { - LOG(Error, "Cannot change actor tag. Given value is invalid."); - return; - } - } - - SetTagIndex(tagIndex); -} - void Actor::SetName(const StringView& value) { if (_name == value) @@ -897,10 +856,21 @@ void Actor::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(StaticFlags, _staticFlags); SERIALIZE(HideFlags); SERIALIZE_MEMBER(Layer, _layer); - if (!other || _tag != other->_tag) + if (!other || Tags != other->Tags) { - stream.JKEY("Tag"); - stream.String(GetTag()); + if (Tags.Count() == 1) + { + stream.JKEY("Tag"); + stream.String(Tags.Get()->ToString()); + } + else + { + stream.JKEY("Tags"); + stream.StartArray(); + for (auto& tag : Tags) + stream.String(tag.ToString()); + stream.EndArray(); + } } if (isPrefabDiff) @@ -996,14 +966,27 @@ void Actor::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) _staticFlags |= StaticFlags::Navigation; } - // Resolve tag (it may be missing in the current configuration const auto tag = stream.FindMember("Tag"); if (tag != stream.MemberEnd()) { if (tag->value.IsString() && tag->value.GetStringLength()) { - const String tagName = tag->value.GetText(); - _tag = Level::GetOrAddTag(tagName); + Tags.Clear(); + Tags.Add(Tags::Get(tag->value.GetText())); + } + } + else + { + const auto tags = stream.FindMember("Tags"); + if (tags != stream.MemberEnd() && tags->value.IsArray()) + { + Tags.Clear(); + for (rapidjson::SizeType i = 0; i < tags->value.Size(); i++) + { + auto& e = tags->value[i]; + if (e.IsString() && e.GetStringLength()) + Tags.Add(Tags::Get(e.GetText())); + } } } diff --git a/Source/Engine/Level/Actor.h b/Source/Engine/Level/Actor.h index 266feda70..1133eceae 100644 --- a/Source/Engine/Level/Actor.h +++ b/Source/Engine/Level/Actor.h @@ -3,6 +3,7 @@ #pragma once #include "SceneObject.h" +#include "Tags.h" #include "Engine/Core/Types/Span.h" #include "Engine/Core/Math/Transform.h" #include "Engine/Core/Math/BoundingBox.h" @@ -34,7 +35,6 @@ API_CLASS(Abstract) class FLAXENGINE_API Actor : public SceneObject friend SceneRendering; friend Prefab; friend PrefabInstanceData; - protected: int16 _isActive : 1; int16 _isActiveInHierarchy : 1; @@ -43,7 +43,6 @@ protected: int16 _drawNoCulling : 1; int16 _drawCategory : 4; byte _layer; - byte _tag; StaticFlags _staticFlags; Transform _localTransform; Transform _transform; @@ -77,6 +76,11 @@ public: API_FIELD(Attributes="HideInEditor, NoSerialize") HideFlags HideFlags; + /// + /// Actor tags collection. + /// + API_FIELD(Attributes="NoAnimate, EditorDisplay(\"General\"), EditorOrder(-68)") Array Tags; + public: /// /// Gets the object layer (index). Can be used for selective rendering or ignoring raycasts. @@ -96,26 +100,10 @@ public: } /// - /// Gets the actor tag (index). Can be used to identify the objects. + /// Sets the layer. /// - FORCE_INLINE int32 GetTagIndex() const - { - return _tag; - } - - /// - /// Determines whether this actor has tag assigned. - /// - API_FUNCTION() FORCE_INLINE bool HasTag() const - { - return _tag != ACTOR_TAG_INVALID; - } - - /// - /// Determines whether this actor has given tag assigned. - /// - /// The tag to check. - API_FUNCTION() bool HasTag(const StringView& tag) const; + /// The index of the layer. + API_PROPERTY() void SetLayer(int32 layerIndex); /// /// Gets the name of the layer. @@ -123,28 +111,21 @@ public: API_PROPERTY() const String& GetLayerName() const; /// - /// Gets the name of the tag. + /// Determines whether this actor has any tag assigned. /// - API_PROPERTY(Attributes="NoAnimate, EditorDisplay(\"General\"), EditorOrder(-68), CustomEditorAlias(\"FlaxEditor.CustomEditors.Editors.ActorTagEditor\")") - const String& GetTag() const; + API_FUNCTION() bool HasTag() const; /// - /// Sets the layer. + /// Determines whether this actor has given tag assigned. /// - /// The index of the layer. - API_PROPERTY() void SetLayer(int32 layerIndex); + /// The tag to check. + API_FUNCTION() bool HasTag(const Tag& tag) const; /// - /// Sets the tag. + /// Determines whether this actor has given tag assigned. /// - /// The index of the tag. - void SetTagIndex(int32 tagIndex); - - /// - /// Sets the tag. - /// - /// Name of the tag. - API_PROPERTY() void SetTag(const StringView& tagName); + /// The tag to check. + API_FUNCTION() bool HasTag(const StringView& tag) const; /// /// Gets the actor name. @@ -955,13 +936,6 @@ public: { } - /// - /// Called when tag gets changed. - /// - virtual void OnTagChanged() - { - } - /// /// Called when adding object to the game. /// diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index b5001a730..338598541 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -166,7 +166,6 @@ Action Level::ScriptsReload; Action Level::ScriptsReloaded; Action Level::ScriptsReloadEnd; #endif -Array Level::Tags; String Level::Layers[32]; bool LevelImpl::spawnActor(Actor* actor, Actor* parent) @@ -226,8 +225,7 @@ void LayersAndTagsSettings::Apply() // Tags/Layers are stored as index in actors so collection change would break the linkage for (auto& tag : Tags) { - if (!Level::Tags.Contains(tag)) - Level::Tags.Add(tag); + Tags::Get(tag); } for (int32 i = 0; i < ARRAY_COUNT(Level::Layers); i++) { @@ -735,17 +733,6 @@ void LevelImpl::CallSceneEvent(SceneEventType eventType, Scene* scene, Guid scen } } -int32 Level::GetOrAddTag(const StringView& tag) -{ - int32 index = Tags.Find(tag); - if (index == INVALID_INDEX) - { - index = Tags.Count(); - Tags.AddOne() = tag; - } - return index; -} - int32 Level::GetNonEmptyLayerNamesCount() { int32 result = 31; diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 96202c830..8039464c5 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -445,23 +445,11 @@ public: static void ConstructParentActorsTreeList(const Array& input, Array& output); public: - /// - /// The tags names. - /// - static Array Tags; - /// /// The layers names. /// static String Layers[32]; - /// - /// Gets or adds the tag (returns the tag index). - /// - /// The tag. - /// The tag index. - static int32 GetOrAddTag(const StringView& tag); - /// /// Gets the amount of non empty layer names (from the beginning, trims the last ones). /// diff --git a/Source/Engine/Level/Tags.cpp b/Source/Engine/Level/Tags.cpp new file mode 100644 index 000000000..83bb2e13e --- /dev/null +++ b/Source/Engine/Level/Tags.cpp @@ -0,0 +1,44 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#include "Tags.h" +#include "Engine/Core/Types/String.h" +#include "Engine/Core/Types/StringView.h" + +Array Tags::List; +#if !BUILD_RELEASE +FLAXENGINE_API String* TagsListDebug = nullptr; +#endif + +const String& Tag::ToString() const +{ + return Index >= 0 && Index < Tags::List.Count() ? Tags::List.Get()[Index] : String::Empty; +} + +bool Tag::operator==(const StringView& other) const +{ + return ToString() == other; +} + +bool Tag::operator!=(const StringView& other) const +{ + return ToString() != other; +} + +Tag Tags::Get(const StringView& tagName) +{ + Tag tag = List.Find(tagName); + if (tag.Index == -1 && tagName.HasChars()) + { + tag.Index = List.Count(); + List.AddOne() = tagName; +#if !BUILD_RELEASE + TagsListDebug = List.Get(); +#endif + } + return tag; +} + +const String& Tags::GetTagName(int32 tag) +{ + return Tag(tag).ToString(); +} diff --git a/Source/Engine/Level/Tags.cs b/Source/Engine/Level/Tags.cs new file mode 100644 index 000000000..0fda5feab --- /dev/null +++ b/Source/Engine/Level/Tags.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +using System; +using System.Runtime.CompilerServices; + +namespace FlaxEngine +{ + partial struct Tag : IEquatable, IEquatable, IComparable, IComparable, IComparable + { + /// + /// The default . + /// + public static Tag Default => new Tag(-1); + + /// + /// Initializes a new instance of the struct. + /// + /// The tag index. + public Tag(int index) + { + Index = index; + } + + [System.Runtime.Serialization.OnDeserializing] + internal void OnDeserializing(System.Runtime.Serialization.StreamingContext context) + { + // Initialize structure with default values to replicate C++ deserialization behavior + this.Index = -1; + } + + /// + /// Compares two tags. + /// + /// The lft tag. + /// The right tag. + /// True if both values are equal, otherwise false. + public static bool operator ==(Tag left, Tag right) + { + return left.Index == right.Index; + } + + /// + /// Compares two tags. + /// + /// The lft tag. + /// The right tag. + /// True if both values are not equal, otherwise false. + public static bool operator !=(Tag left, Tag right) + { + return left.Index == right.Index; + } + + /// + /// Checks if tag is valid. + /// + /// The tag to check. + /// True if tag is valid, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(Tag tag) + { + return tag.Index != -1; + } + + /// + public int CompareTo(object obj) + { + if (obj is string asString) + return CompareTo(asString); + if (obj is Tag asTag) + return CompareTo(asTag); + return 0; + } + + /// + public bool Equals(Tag other) + { + return Index == other.Index; + } + + /// + public bool Equals(string other) + { + return string.Equals(ToString(), other, StringComparison.Ordinal); + } + + /// + public int CompareTo(Tag other) + { + return string.Compare(ToString(), ToString(), StringComparison.Ordinal); + } + + /// + public int CompareTo(string other) + { + return string.Compare(ToString(), other, StringComparison.Ordinal); + } + + /// + public override bool Equals(object obj) + { + return obj is Tag other && Index == other.Index; + } + + /// + public override int GetHashCode() + { + return Index; + } + + /// + public override string ToString() + { + return Tags.Internal_GetTagName(Index); + } + } +} diff --git a/Source/Engine/Level/Tags.h b/Source/Engine/Level/Tags.h new file mode 100644 index 000000000..5616627c9 --- /dev/null +++ b/Source/Engine/Level/Tags.h @@ -0,0 +1,88 @@ +// Copyright (c) 2012-2022 Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Types/BaseTypes.h" +#include "Engine/Core/Collections/Array.h" + +/// +/// Gameplay tag that represents a hierarchical name of the form 'X.Y.Z' (namespaces separated with a dot). Tags are defined in project LayersAndTagsSettings asset but can be also created from code. +/// +API_STRUCT(NoDefault) struct FLAXENGINE_API Tag +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(Tag); + + /// + /// Index of the tag (in global Level.Tags list). + /// + API_FIELD() int32 Index = -1; + + /// + /// Gets the tag name. + /// + const String& ToString() const; + +public: + Tag() = default; + + FORCE_INLINE Tag(int32 index) + : Index(index) + { + } + + FORCE_INLINE operator bool() const + { + return Index != -1; + } + + FORCE_INLINE bool operator==(const Tag& other) const + { + return Index == other.Index; + } + + FORCE_INLINE bool operator!=(const Tag& other) const + { + return Index != other.Index; + } + + bool operator==(const StringView& other) const; + bool operator!=(const StringView& other) const; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; +}; + +inline uint32 GetHash(const Tag& key) +{ + return (uint32)key.Index; +} + +/// +/// Gameplay tags utilities. +/// +API_CLASS(Static) class FLAXENGINE_API Tags +{ + DECLARE_SCRIPTING_TYPE_MINIMAL(Tags); + + /// + /// List of all tags. + /// + API_FIELD(ReadOnly) static Array List; + + /// + /// Gets or adds the tag. + /// + /// The tag name. + /// The tag. + API_FUNCTION() static Tag Get(const StringView& tagName); + +private: + API_FUNCTION(NoProxy) static const String& GetTagName(int32 tag); +}; + +#if !BUILD_RELEASE +extern FLAXENGINE_API String* TagsListDebug; // Used by flax.natvis +#endif diff --git a/Source/flax.natvis b/Source/flax.natvis index 193efe0cc..70f9b15e3 100644 --- a/Source/flax.natvis +++ b/Source/flax.natvis @@ -215,4 +215,10 @@ + + + None + Tag={TagsListDebug[Index]} + +