// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors { /// /// The per-feature flags for custom editors system. /// [HideInEditor, Flags] public enum FeatureFlags { /// /// Nothing. /// None = 0, /// /// Enables caching the expanded groups in this presenter. Used to preserve the expanded groups using project cache. /// CacheExpandedGroups = 1 << 0, /// /// Enables using prefab-related features of the properties editor (eg. revert to prefab option). /// UsePrefab = 1 << 1, /// /// Enables using default-value-related features of the properties editor (eg. revert to default option). /// 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. /// /// [HideInEditor] public class CustomEditorPresenter : LayoutElementsContainer { /// /// The panel control. /// /// public class PresenterPanel : VerticalPanel { private CustomEditorPresenter _presenter; /// /// Gets the presenter. /// public CustomEditorPresenter Presenter => _presenter; internal PresenterPanel(CustomEditorPresenter presenter) { _presenter = presenter; AnchorPreset = AnchorPresets.StretchAll; Offsets = Margin.Zero; IsScrollable = true; } /// public override void Update(float deltaTime) { try { // Update editors _presenter.Update(); } catch (Exception ex) { FlaxEditor.Editor.LogWarning(ex); } base.Update(deltaTime); } /// public override void OnDestroy() { base.OnDestroy(); _presenter = null; } } /// /// The root editor. Mocks some custom editors events. Created a child editor for the selected objects. /// /// protected class RootEditor : SyncPointEditor { private CustomEditor _overrideEditor; /// /// The selected objects editor. /// public CustomEditor Editor; /// /// Gets or sets the override custom editor used to edit selected objects. /// public CustomEditor OverrideEditor { get => _overrideEditor; set { _overrideEditor = value; RebuildLayout(); } } /// /// The text to show when no object is selected. /// public string NoSelectionText; /// /// Initializes a new instance of the class. /// /// The text to show when no item is selected. public RootEditor(string noSelectionText) { NoSelectionText = noSelectionText ?? "No selection"; } /// /// Setups editor for selected objects. /// /// The presenter. public void Setup(CustomEditorPresenter presenter) { Cleanup(); Initialize(presenter, presenter, null); } /// public override void Initialize(LayoutElementsContainer layout) { Presenter.BeforeLayout?.Invoke(layout); var selection = Presenter.Selection; selection.ClearReferenceValue(); if (selection.Count > 0) { if (_overrideEditor != null) { Editor = _overrideEditor; } else { var type = ScriptType.Object; if (selection.HasDifferentTypes == false) type = TypeUtils.GetObjectType(selection[0]); Editor = CustomEditorsUtil.CreateEditor(type, false); } Editor.Initialize(Presenter, Presenter, selection); OnChildCreated(Editor); } else { var label = layout.Label(NoSelectionText, TextAlignment.Center); label.Label.Height = 20.0f; } base.Initialize(layout); Presenter.AfterLayout?.Invoke(layout); } /// protected override void Deinitialize() { Editor = null; _overrideEditor = null; base.Deinitialize(); } /// protected override void OnModified() { Presenter.OnModified(); base.OnModified(); } } /// /// The panel. /// public readonly PresenterPanel Panel; /// /// The selected objects editor (root, it generates actual editor for selection). /// protected readonly RootEditor Editor; /// /// The selected objects list (read-only). /// public readonly ValueContainer Selection = new ValueContainer(ScriptMemberInfo.Null); /// /// The undo object used by this editor. /// public readonly Undo Undo; /// /// Occurs when selection gets changed. /// public event Action SelectionChanged; /// /// Occurs when any property gets changed. /// public event Action Modified; /// /// Occurs when presenter wants to gather undo objects to record changes. Can be overriden to provide custom objects collection. /// public Func> GetUndoObjects = presenter => presenter.Selection; /// /// Gets the amount of objects being selected. /// public int SelectionCount => Selection.Count; /// /// Gets or sets the override custom editor used to edit selected objects. /// public CustomEditor OverrideEditor { get => Editor.OverrideEditor; set => Editor.OverrideEditor = value; } /// /// Gets the root editor. /// public CustomEditor Root => Editor; /// /// Gets a value indicating whether build on update flag is set and layout will be updated during presenter update. /// public bool BuildOnUpdate => _buildOnUpdate; /// /// The features to use for properties editor. /// public FeatureFlags Features = FeatureFlags.UsePrefab | FeatureFlags.UseDefault; /// /// Occurs when before creating layout for the selected objects editor UI. Can be used to inject custom UI to the layout. /// public event Action BeforeLayout; /// /// Occurs when after creating layout for the selected objects editor UI. Can be used to inject custom UI to the layout. /// public event Action AfterLayout; /// /// The Editor context that owns this presenter. Can be or or other window/panel - custom editor scan use it for more specific features. /// public IPresenterOwner Owner; /// /// Gets or sets the text to show when no object is selected. /// public string NoSelectionText { get => Editor.NoSelectionText; set { Editor.NoSelectionText = value; if (SelectionCount == 0) BuildLayoutOnUpdate(); } } /// /// 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. /// /// 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, IPresenterOwner owner = null) { Undo = undo; Owner = owner; Panel = new PresenterPanel(this); Editor = new RootEditor(noSelectionText); Editor.Initialize(this, this, null); } /// /// Selects the specified object. /// /// The object. public void Select(object obj) { if (obj == null) { Deselect(); return; } if (Selection.Count == 1 && Selection[0] == obj) return; Selection.Clear(); Selection.Add(obj); Selection.SetType(new ScriptType(obj.GetType())); OnSelectionChanged(); } /// /// Selects the specified objects. /// /// The objects. public void Select(IEnumerable objects) { if (objects == null) { Deselect(); return; } var objectsArray = objects as object[] ?? objects.ToArray(); if (Utils.ArraysEqual(objectsArray, Selection)) return; Selection.Clear(); Selection.AddRange(objectsArray); Selection.SetType(new ScriptType(objectsArray.GetType())); OnSelectionChanged(); } /// /// Clears the selected objects. /// public void Deselect() { if (Selection.Count == 0) return; Selection.Clear(); Selection.SetType(ScriptType.Null); OnSelectionChanged(); } /// /// Builds the editors layout. /// public virtual void BuildLayout() { // Clear layout var panel = Panel.Parent as Panel; var parentScrollV = panel?.VScrollBar?.Value ?? -1; Panel.IsLayoutLocked = true; Panel.DisposeChildren(); ClearLayout(); Editor.Setup(this); Panel.IsLayoutLocked = false; Panel.PerformLayout(); // Restore scroll value if (parentScrollV > -1) panel.VScrollBar.Value = parentScrollV; if (_readOnly) UpdateReadOnly(); } /// /// Sets the request to build the editor layout on the next update. /// public void BuildLayoutOnUpdate() { _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) { if (open) group.Panel.Open(false); else group.Panel.Close(false); } foreach (var child in c.Children) { if (child is LayoutElementsContainer cc) ExpandGroups(cc, open); } } /// /// Expands all the groups in this editor. /// public void OpenAllGroups() { ExpandGroups(this, true); } /// /// Closes all the groups in this editor. /// public void ClosesAllGroups() { ExpandGroups(this, false); } /// /// Invokes event. /// public void OnModified() { Modified?.Invoke(); } /// /// Called when selection gets changed. /// protected virtual void OnSelectionChanged() { BuildLayout(); SelectionChanged?.Invoke(); } /// /// Updates custom editors. Refreshes UI values and applies changes to the selected objects. /// internal void Update() { if (_buildOnUpdate) { _buildOnUpdate = false; BuildLayout(); } Editor?.RefreshInternal(); } /// public override ContainerControl ContainerControl => Panel; } }