From 0f5a177be2b2237f4e5c08c8e30a18b27dfbe88b Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 14 Oct 2023 21:32:58 -0500 Subject: [PATCH] Improve collection editor. --- .../CustomEditors/Editors/CollectionEditor.cs | 301 ++++++++++++++---- .../Editors/ModelInstanceEntryEditor.cs | 18 +- .../Core/Config/LayersAndTagsSettings.cs | 2 +- .../Localization/LocalizationSettings.h | 2 +- .../Attributes/CollectionAttribute.cs | 26 ++ 5 files changed, 281 insertions(+), 68 deletions(-) diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 8922e2d25..281f6b206 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -3,9 +3,9 @@ using System; using System.Collections; using System.Linq; -using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Input; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; @@ -34,6 +34,9 @@ namespace FlaxEditor.CustomEditors.Editors /// The index of the item (zero-based). /// public readonly int Index; + + private Image _moveUpImage; + private Image _moveDownImage; /// /// Initializes a new instance of the class. @@ -46,13 +49,61 @@ namespace FlaxEditor.CustomEditors.Editors Editor = editor; Index = index; + var icons = FlaxEditor.Editor.Instance.Icons; + + var style = FlaxEngine.GUI.Style.Current; + var imageSize = 18 - Margin.Height; + _moveDownImage = new Image + { + Brush = new SpriteBrush(icons.Down32), + TooltipText = "Move down", + IsScrollable = false, + AnchorPreset = AnchorPresets.MiddleLeft, + Bounds = new Rectangle(imageSize + 2, -Height * 0.5f, imageSize, imageSize), + Color = style.ForegroundGrey, + Margin = new Margin(1), + Parent = this, + }; + _moveDownImage.Clicked += MoveDownImageOnClicked; + _moveDownImage.Enabled = Index + 1 < Editor.Count; + + _moveUpImage = new Image + { + Brush = new SpriteBrush(icons.Up32), + TooltipText = "Move up", + IsScrollable = false, + AnchorPreset = AnchorPresets.MiddleLeft, + Bounds = new Rectangle(0, -Height * 0.5f, imageSize, imageSize), + Color = style.ForegroundGrey, + Margin = new Margin(1), + Parent = this, + }; + _moveUpImage.Clicked += MoveUpImageOnClicked; + _moveUpImage.Enabled = Index > 0; + + Margin = new Margin(_moveDownImage.Right + 2, Margin.Right, Margin.Top, Margin.Bottom); SetupContextMenu += OnSetupContextMenu; } + + private void MoveUpImageOnClicked(Image image, MouseButton button) + { + OnMoveUpClicked(); + } + + private void MoveDownImageOnClicked(Image image, MouseButton button) + { + OnMoveDownClicked(); + } private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor) { + menu.ItemsContainer.RemoveChildren(); + + menu.AddButton("Copy", linkedEditor.Copy); + var paste = menu.AddButton("Paste", linkedEditor.Paste); + paste.Enabled = linkedEditor.CanPaste; + menu.AddSeparator(); - var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked); moveUpButton.Enabled = Index > 0; @@ -62,17 +113,140 @@ namespace FlaxEditor.CustomEditors.Editors menu.AddButton("Remove", OnRemoveClicked); } - private void OnMoveUpClicked(ContextMenuButton button) + private void OnMoveUpClicked() { Editor.Move(Index, Index - 1); } - private void OnMoveDownClicked(ContextMenuButton button) + private void OnMoveDownClicked() { Editor.Move(Index, Index + 1); } - private void OnRemoveClicked(ContextMenuButton button) + private void OnRemoveClicked() + { + Editor.Remove(Index); + } + } + + private class CollectionDropPanel : DropPanel + { + /// + /// The collection editor. + /// + public CollectionEditor Editor; + + /// + /// The index of the item (zero-based). + /// + public int Index { get; private set; } + + /// + /// The linked editor. + /// + public CustomEditor LinkedEditor; + + private bool _canReorder = true; + private Image _moveUpImage; + private Image _moveDownImage; + + public void Setup(CollectionEditor editor, int index, bool canReorder = true) + { + HeaderHeight = 18; + _canReorder = canReorder; + EnableDropDownIcon = true; + var icons = FlaxEditor.Editor.Instance.Icons; + ArrowImageClosed = new SpriteBrush(icons.ArrowRight12); + ArrowImageOpened = new SpriteBrush(icons.ArrowDown12); + HeaderText = $"Element {index}"; + IsClosed = false; + Editor = editor; + Index = index; + Offsets = new Margin(7, 7, 0, 0); + + MouseButtonRightClicked += OnMouseButtonRightClicked; + if (_canReorder) + { + var imageSize = HeaderHeight; + var style = FlaxEngine.GUI.Style.Current; + _moveDownImage = new Image + { + Brush = new SpriteBrush(icons.Down32), + TooltipText = "Move down", + IsScrollable = false, + Bounds = new Rectangle(imageSize * 2 + ItemsMargin.Left + 2, -HeaderHeight, imageSize, imageSize), + Color = style.ForegroundGrey, + Margin = new Margin(1), + Parent = this, + }; + _moveDownImage.Clicked += MoveDownImageOnClicked; + _moveDownImage.Enabled = Index + 1 < Editor.Count; + + _moveUpImage = new Image + { + Brush = new SpriteBrush(icons.Up32), + TooltipText = "Move up", + IsScrollable = false, + Bounds = new Rectangle(imageSize + ItemsMargin.Left, -HeaderHeight, imageSize, imageSize), + Color = style.ForegroundGrey, + Margin = new Margin(1), + Parent = this, + }; + _moveUpImage.Clicked += MoveUpImageOnClicked; + _moveUpImage.Enabled = Index > 0; + + HeaderTextMargin = new Margin(_moveDownImage.Right - 12, HeaderTextMargin.Right, HeaderTextMargin.Top, HeaderTextMargin.Bottom); + } + } + + private void MoveUpImageOnClicked(Image image, MouseButton button) + { + OnMoveUpClicked(); + } + + private void MoveDownImageOnClicked(Image image, MouseButton button) + { + OnMoveDownClicked(); + } + + private void OnMouseButtonRightClicked(DropPanel panel, Float2 location) + { + if (LinkedEditor == null) + return; + var linkedEditor = LinkedEditor; + var menu = new ContextMenu(); + + menu.AddButton("Copy", linkedEditor.Copy); + var paste = menu.AddButton("Paste", linkedEditor.Paste); + paste.Enabled = linkedEditor.CanPaste; + + if (_canReorder) + { + menu.AddSeparator(); + + var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked); + moveUpButton.Enabled = Index > 0; + + var moveDownButton = menu.AddButton("Move down", OnMoveDownClicked); + moveDownButton.Enabled = Index + 1 < Editor.Count; + } + + menu.AddButton("Remove", OnRemoveClicked); + + menu.Show(panel, location); + } + + private void OnMoveUpClicked() + { + Editor.Move(Index, Index - 1); + } + + private void OnMoveDownClicked() + { + Editor.Move(Index, Index + 1); + } + + private void OnRemoveClicked() { Editor.Remove(Index); } @@ -82,12 +256,13 @@ namespace FlaxEditor.CustomEditors.Editors /// Determines if value of collection can be null. /// protected bool NotNullItems; - - private IntegerValueElement _size; + + private IntValueBox _sizeBox; private Color _background; private int _elementsCount; private bool _readOnly; private bool _canReorderItems; + private CollectionAttribute.DisplayType _displayType; /// /// Gets the length of the collection. @@ -117,12 +292,13 @@ namespace FlaxEditor.CustomEditors.Editors _readOnly = false; _canReorderItems = true; _background = FlaxEngine.GUI.Style.Current.CollectionBackgroundColor; + _displayType = CollectionAttribute.DisplayType.Header; NotNullItems = false; // Try get CollectionAttribute for collection editor meta var attributes = Values.GetAttributes(); Type overrideEditorType = null; - float spacing = 10.0f; + float spacing = 1.0f; var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute); if (collection != null) { @@ -133,20 +309,40 @@ namespace FlaxEditor.CustomEditors.Editors _background = collection.BackgroundColor.Value; overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type; spacing = collection.Spacing; + _displayType = collection.Display; } // Size - if (_readOnly || (NotNullItems && size == 0)) + if (layout.ContainerControl is DropPanel dropPanel) { - layout.Label("Size", size.ToString()); - } - else - { - _size = layout.IntegerValue("Size"); - _size.IntValue.MinValue = 0; - _size.IntValue.MaxValue = ushort.MaxValue; - _size.IntValue.Value = size; - _size.IntValue.EditEnd += OnSizeChanged; + var height = dropPanel.HeaderHeight - dropPanel.HeaderTextMargin.Height; + var y = -dropPanel.HeaderHeight + dropPanel.HeaderTextMargin.Top; + _sizeBox = new IntValueBox(size) + { + MinValue = 0, + MaxValue = ushort.MaxValue, + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-40 - dropPanel.ItemsMargin.Right, y, 40, height), + Parent = dropPanel, + }; + + var label = new Label + { + Text = "Size", + AnchorPreset = AnchorPresets.TopRight, + Bounds = new Rectangle(-_sizeBox.Width - 40 - dropPanel.ItemsMargin.Right - 2, y, 40, height), + Parent = dropPanel + }; + + if (_readOnly || (NotNullItems && size == 0)) + { + _sizeBox.IsReadOnly = true; + _sizeBox.Enabled = false; + } + else + { + _sizeBox.EditEnd += OnSizeChanged; + } } // Elements @@ -155,55 +351,42 @@ namespace FlaxEditor.CustomEditors.Editors var panel = layout.VerticalPanel(); panel.Panel.BackgroundColor = _background; var elementType = ElementType; + bool single = elementType.IsPrimitive || + elementType.Equals(new ScriptType(typeof(string))) || + elementType.IsEnum || + (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) || + (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) || + elementType.Equals(new ScriptType(typeof(JsonAsset))) || + elementType.Equals(new ScriptType(typeof(SettingsBase))); // Use separate layout cells for each collection items to improve layout updates for them in separation var useSharedLayout = elementType.IsPrimitive || elementType.IsEnum; - if (_canReorderItems) + for (int i = 0; i < size; i++) { - for (int i = 0; i < size; i++) + if (i > 0 && i < size && spacing > 0) { - if (i != 0 && spacing > 0f) - { - if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement) - { - if (propertiesListElement.Labels.Count > 0) - { - var label = propertiesListElement.Labels[propertiesListElement.Labels.Count - 1]; - var margin = label.Margin; - margin.Bottom += spacing; - label.Margin = margin; - } - propertiesListElement.Space(spacing); - } - else - { - panel.Space(spacing); - } - } - - var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; - var property = panel.AddPropertyItem(new CollectionItemLabel(this, i)); - var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel(); - itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); + panel.Space(spacing); } - } - else - { - for (int i = 0; i < size; i++) + var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; + if (_displayType == CollectionAttribute.DisplayType.Inline || (collection == null && single) || (_displayType == CollectionAttribute.DisplayType.Default && single)) { - if (i != 0 && spacing > 0f) - { - if (panel.Children.Count > 0 && panel.Children[panel.Children.Count - 1] is PropertiesListElement propertiesListElement) - propertiesListElement.Space(spacing); - else - panel.Space(spacing); - } - - var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null; - var property = panel.AddPropertyItem("Element " + i); + PropertyNameLabel itemLabel; + if (_canReorderItems) + itemLabel = new CollectionItemLabel(this, i); + else + itemLabel = new PropertyNameLabel("Element " + i); + var property = panel.AddPropertyItem(itemLabel); var itemLayout = useSharedLayout ? (LayoutElementsContainer)property : property.VerticalPanel(); - itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); + itemLabel.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); + itemLabel.Parent.Offsets = new Margin(7, 7, 0, 0); + } + else if (_displayType == CollectionAttribute.DisplayType.Header || (_displayType == CollectionAttribute.DisplayType.Default && !single)) + { + var cdp = panel.CustomContainer(); + cdp.CustomControl.Setup(this, i, _canReorderItems); + var itemLayout = useSharedLayout ? (LayoutElementsContainer)cdp : cdp.VerticalPanel(); + cdp.CustomControl.LinkedEditor = itemLayout.Object(new ListValueContainer(elementType, i, Values, attributes), overrideEditor); } } } @@ -269,7 +452,7 @@ namespace FlaxEditor.CustomEditors.Editors if (IsSetBlocked) return; - Resize(_size.IntValue.Value); + Resize(_sizeBox.Value); } /// diff --git a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs index 9607680f2..3f55506fb 100644 --- a/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ModelInstanceEntryEditor.cs @@ -4,6 +4,7 @@ using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; +using FlaxEngine.GUI; namespace FlaxEditor.CustomEditors.Editors { @@ -13,7 +14,7 @@ namespace FlaxEditor.CustomEditors.Editors [CustomEditor(typeof(ModelInstanceEntry)), DefaultEditor] public sealed class ModelInstanceEntryEditor : GenericEditor { - private GroupElement _group; + private DropPanel _mainPanel; private bool _updateName; private int _entryIndex; private bool _isRefreshing; @@ -25,8 +26,11 @@ namespace FlaxEditor.CustomEditors.Editors public override void Initialize(LayoutElementsContainer layout) { _updateName = true; - var group = layout.Group("Entry"); - _group = group; + if (layout.ContainerControl.Parent is DropPanel panel) + { + _mainPanel = panel; + _mainPanel.HeaderText = "Entry"; + } if (ParentEditor == null) return; @@ -56,14 +60,14 @@ namespace FlaxEditor.CustomEditors.Editors // Create material picker var materialValue = new CustomValueContainer(new ScriptType(typeof(MaterialBase)), _material, (instance, index) => _material, (instance, index, value) => _material = value as MaterialBase); - var materialEditor = (AssetRefEditor)_group.Property(materialLabel, materialValue); + var materialEditor = (AssetRefEditor)layout.Property(materialLabel, materialValue); materialEditor.Values.SetDefaultValue(defaultValue); materialEditor.RefreshDefaultValue(); materialEditor.Picker.SelectedItemChanged += OnSelectedMaterialChanged; _materialEditor = materialEditor; } - base.Initialize(group); + base.Initialize(layout); } private void OnSelectedMaterialChanged() @@ -116,7 +120,7 @@ namespace FlaxEditor.CustomEditors.Editors { // Update panel title to match material slot name if (_updateName && - _group != null && + _mainPanel != null && ParentEditor?.ParentEditor != null && ParentEditor.ParentEditor.Values.Count > 0) { @@ -127,7 +131,7 @@ namespace FlaxEditor.CustomEditors.Editors if (slots != null && slots.Length > entryIndex) { _updateName = false; - _group.Panel.HeaderText = "Entry " + slots[entryIndex].Name; + _mainPanel.HeaderText = "Entry " + slots[entryIndex].Name; } } } diff --git a/Source/Engine/Core/Config/LayersAndTagsSettings.cs b/Source/Engine/Core/Config/LayersAndTagsSettings.cs index 008f70504..d5ebbe19b 100644 --- a/Source/Engine/Core/Config/LayersAndTagsSettings.cs +++ b/Source/Engine/Core/Config/LayersAndTagsSettings.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.Content.Settings /// /// The layers names. /// - [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true)] + [EditorOrder(10), EditorDisplay("Layers", EditorDisplayAttribute.InlineStyle), Collection(ReadOnly = true, Display = CollectionAttribute.DisplayType.Inline)] public string[] Layers = new string[32]; /// diff --git a/Source/Engine/Localization/LocalizationSettings.h b/Source/Engine/Localization/LocalizationSettings.h index e2c546c8b..22a4f03e4 100644 --- a/Source/Engine/Localization/LocalizationSettings.h +++ b/Source/Engine/Localization/LocalizationSettings.h @@ -16,7 +16,7 @@ public: /// /// The list of the string localization tables used by the game. /// - API_FIELD() + API_FIELD(Attributes="Collection(Display = CollectionAttribute.DisplayType.Inline)") Array> LocalizedStringTables; /// diff --git a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs index ca8f45d39..cd2e962e6 100644 --- a/Source/Engine/Scripting/Attributes/CollectionAttribute.cs +++ b/Source/Engine/Scripting/Attributes/CollectionAttribute.cs @@ -10,6 +10,32 @@ namespace FlaxEngine [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Class)] public sealed class CollectionAttribute : Attribute { + /// + /// The display type for collections. + /// + public enum DisplayType + { + /// + /// Displays the default display type. + /// + Default, + + /// + /// Displays a header. + /// + Header, + + /// + /// Displays inline. + /// + Inline, + } + + /// + /// Gets or sets the display type. + /// + public DisplayType Display; + /// /// Gets or sets whether this collection is read-only. If true, applications using this collection should not allow to add or remove items. ///