// Copyright (c) Wojciech Figat. All rights reserved. using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors { /// /// Custom editor layout style modes. /// [HideInEditor] public enum DisplayStyle { /// /// Creates a separate group for the editor (drop down element). This is a default value. /// Group, /// /// Inlines editor contents into the property area without creating a drop-down group. /// Inline, /// /// Inlines editor contents into the parent editor layout. Won;t use property with label name. /// InlineIntoParent, } /// /// Base class for all custom editors used to present object(s) properties. Allows to extend game objects editing with more logic and customization. /// [HideInEditor] public abstract class CustomEditor { /// /// True if Editor is during value setting (eg. by user or from copy/paste). /// public static bool IsSettingValue = false; private LayoutElementsContainer _layout; private CustomEditorPresenter _presenter; private CustomEditor _parent; private readonly List _children = new List(); private ValueContainer _values; private bool _isSetBlocked; private bool _skipChildrenRefresh; private bool _hasValueDirty; private bool _rebuildOnRefresh; private object _valueToSet; /// /// Gets the custom editor style. /// public virtual DisplayStyle Style => DisplayStyle.Group; /// /// Gets a value indicating whether single object is selected. /// public bool IsSingleObject => _values.IsSingleObject; /// /// Gets a value indicating whether selected objects are different values. /// public bool HasDifferentValues => _values.HasDifferentValues; /// /// Gets a value indicating whether selected objects are different types. /// public bool HasDifferentTypes => _values.HasDifferentTypes; /// /// Gets the values types array (without duplicates). /// public ScriptType[] ValuesTypes => _values.ValuesTypes; /// /// Gets the values. /// public ValueContainer Values => _values; /// /// Gets the parent editor. /// public CustomEditor ParentEditor => _parent; /// /// Gets the children editors (readonly). /// public List ChildrenEditors => _children; /// /// Gets the presenter control. It's the top most part of the Custom Editors layout. /// public CustomEditorPresenter Presenter => _presenter; /// /// Gets a value indicating whether setting value is blocked (during refresh). /// protected bool IsSetBlocked => _isSetBlocked; /// /// The linked label used to show this custom editor. Can be null if not used (eg. editor is inlined or is using a very customized UI layout). /// public PropertyNameLabel LinkedLabel; /// /// Gets the layout for this editor. Used to calculate bounds. /// public LayoutElementsContainer Layout => _layout; internal virtual void Initialize(CustomEditorPresenter presenter, LayoutElementsContainer layout, ValueContainer values) { _layout = layout; _presenter = presenter; _values = values; var prev = CurrentCustomEditor; CurrentCustomEditor = this; _isSetBlocked = true; Initialize(layout); ShowButtons(); Refresh(); _isSetBlocked = false; CurrentCustomEditor = prev; } private void ShowButtons() { var values = Values; if (values == null || values.HasDifferentTypes) return; var type = TypeUtils.GetObjectType(values[0]); var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (var method in methods) { if (!method.HasAttribute(typeof(ButtonAttribute)) || method.ParametersCount != 0) continue; var attribute = method.GetAttribute(); var text = string.IsNullOrEmpty(attribute.Text) ? Utilities.Utils.GetPropertyNameUI(method.Name) : attribute.Text; var tooltip = string.IsNullOrEmpty(attribute.Tooltip) ? Editor.Instance.CodeDocs.GetTooltip(method) : attribute.Tooltip; var button = _layout.Button(text, tooltip); button.Button.Tag = method; button.Button.ButtonClicked += OnButtonClicked; } } private void OnButtonClicked(Button button) { var method = (ScriptMemberInfo)button.Tag; var obj = method.IsStatic ? null : Values[0]; method.Invoke(obj); } internal static CustomEditor CurrentCustomEditor; internal void OnChildCreated(CustomEditor child) { // Link _children.Add(child); child._parent = this; } /// /// Rebuilds the editor layout. Cleans the whole UI with child elements/editors and then builds new hierarchy. Call it only when necessary. /// public void RebuildLayout() { // Skip rebuilding during init if (CurrentCustomEditor == this) return; // Special case for root objects to run normal layout build if (_presenter != null && _presenter.Selection == Values) { _presenter.BuildLayout(); return; } 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; control.IsLayoutLocked = true; control.DisposeChildren(); layout.ClearLayout(); Cleanup(); _parent = parent; Initialize(presenter, layout, values); control.IsLayoutLocked = false; control.PerformLayout(); // Restore scroll value if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null) panel.VScrollBar.Value = parentScrollV; } /// /// Rebuilds the editor layout on editor refresh. Postponed after dirty value gets synced. Call it after to update editor after actual value assign. /// public void RebuildLayoutOnRefresh() { _rebuildOnRefresh = true; } /// /// Sets the request to skip the custom editor children refresh during next update. Can be used when editor layout has to be rebuild during the update itself. /// public void SkipChildrenRefresh() { _skipChildrenRefresh = true; } /// /// Initializes this editor. /// /// The layout builder. public abstract void Initialize(LayoutElementsContainer layout); /// /// Deinitializes this editor (unbind events and cleanup). /// protected virtual void Deinitialize() { } /// /// Cleanups this editor resources and child editors. /// internal void Cleanup() { Deinitialize(); for (int i = 0; i < _children.Count; i++) { _children[i].Cleanup(); } _children.Clear(); _presenter = null; _parent = null; _hasValueDirty = false; _valueToSet = null; _values = null; _isSetBlocked = false; _rebuildOnRefresh = false; LinkedLabel = null; } internal void RefreshRoot() { try { for (int i = 0; i < _children.Count; i++) _children[i].RefreshRootChild(); } catch (TargetException ex) { // This happens when something (from root editor) calls the error. // Just handle it and rebuild UI. Some parts of the pipeline can report data problems via that exception. Editor.LogWarning("Exception while updating the root editors"); Editor.LogWarning(ex); Presenter.BuildLayoutOnUpdate(); } } internal virtual void RefreshRootChild() { // Check if need to update value if (_hasValueDirty) { IsSettingValue = true; try { // Cleanup (won't retry update in case of exception) object val = _valueToSet; _hasValueDirty = false; _valueToSet = null; // Assign value if (val is IList l && l.Count == _values.Count && _values.Count > 1) { for (int i = 0; i < _values.Count; i++) _values[i] = l[i]; } else { for (int i = 0; i < _values.Count; i++) _values[i] = val; } } finally { OnUnDirty(); IsSettingValue = false; } } Refresh(); for (int i = 0; i < _children.Count; i++) _children[i].RefreshInternal(); } /// /// Synchronizes the value of the container. Called during Refresh to flush property after editing it in UI. /// /// The value to set. protected virtual void SynchronizeValue(object value) { _values.Set(_parent.Values, value); } private bool SyncParent() { // TODO: add attribute for types that want to sync their contents with a parent var type = Values.Type.Type; if (type == typeof(LocalizedString) || type == typeof(FontReference)) return true; return _parent != null && !(_parent is SyncPointEditor); } internal virtual void RefreshInternal() { if (_values == null) return; // Check if need to update value if (_hasValueDirty) { IsSettingValue = true; try { // Cleanup (won't retry update in case of exception) object val = _valueToSet; _hasValueDirty = false; _valueToSet = null; // Assign value SynchronizeValue(val); // Propagate values up (eg. when member of structure gets modified, also structure should be updated as a part of the other object) var obj = _parent; while (obj.SyncParent()) { obj.Values.Set(obj._parent.Values, obj.Values); obj = obj._parent; } } finally { OnUnDirty(); IsSettingValue = false; } } else { // Update values _values.Refresh(_parent.Values); } // Update itself _isSetBlocked = true; Refresh(); _isSetBlocked = false; // Update children if (_skipChildrenRefresh) { _skipChildrenRefresh = false; return; } try { for (int i = 0; i < _children.Count; i++) _children[i].RefreshInternal(); } catch (TargetException ex) { // This happens when one of the edited objects changes its type. // Eg. from Label to TextBox, while both types were assigned to the same field of type Control. // It's valid, just rebuild the child editors and log the warning to keep it tracking. Editor.LogWarning("Exception while updating the child editors"); Editor.LogWarning(ex); RebuildLayoutOnRefresh(); } // Rebuild if flag is set if (_rebuildOnRefresh) { _rebuildOnRefresh = false; RebuildLayout(); } } /// /// Refreshes this editor. /// public virtual void Refresh() { if (LinkedLabel != null) { var color = Color.Transparent; if (Values.HasReferenceValue && CanRevertReferenceValue) color = FlaxEngine.GUI.Style.Current.BackgroundSelected; else if (Values.HasDefaultValue && CanRevertDefaultValue) color = Color.Yellow * 0.8f; LinkedLabel.HighlightStripColor = color; // Grey out deprecated members if (Values.IsObsolete) LinkedLabel.TextColor = LinkedLabel.TextColorHighlighted = FlaxEngine.GUI.Style.Current.ForegroundGrey; } } internal void LinkLabel(PropertyNameLabel label) { LinkedLabel = label; } internal bool CanEditValue { get { var readOnly = Values.Info.GetAttribute(); return readOnly == null; } } /// /// If true, the value reverting to default/reference will be handled via iteration over children editors, instead of for a whole object at once. /// public virtual bool RevertValueWithChildren => ChildrenEditors.Count != 0; private void RevertDiffToDefault() { if (RevertValueWithChildren) { foreach (var child in ChildrenEditors) child.RevertDiffToDefault(); } else { if (Values.IsDefaultValueModified) SetValueToDefault(); } } /// /// Gets a value indicating whether this editor can revert the value to default value. /// public bool CanRevertDefaultValue { get { if (!Values.IsDefaultValueModified) return false; return CanEditValue; } } /// /// Reverts the property value to default value (if has). Handles undo. /// public void RevertToDefaultValue() { if (!Values.HasDefaultValue || !CanEditValue) return; RevertDiffToDefault(); } /// /// Updates the default value assigned to the editor's values container. Sends the event down the custom editors hierarchy to propagate the change. /// /// /// Has no effect on editors that don't have default value assigned. /// public void RefreshDefaultValue() { if (!Values.HasDefaultValue) return; if (ParentEditor?.Values?.HasDefaultValue ?? false) { Values.RefreshDefaultValue(ParentEditor.Values.DefaultValue); } for (int i = 0; i < ChildrenEditors.Count; i++) { ChildrenEditors[i].RefreshDefaultValue(); } } /// /// Clears all the default value of the container in the whole custom editors tree (this container and all children). /// public void ClearDefaultValueAll() { Values.ClearDefaultValue(); for (int i = 0; i < ChildrenEditors.Count; i++) { ChildrenEditors[i].ClearDefaultValueAll(); } } private void RevertDiffToReference() { if (RevertValueWithChildren) { foreach (var child in ChildrenEditors) child.RevertDiffToReference(); } else { if (CanRevertReferenceValue) SetValueToReference(); } } /// /// Gets a value indicating whether this editor can revert the value to reference value. /// public bool CanRevertReferenceValue { get { if (!Values.IsReferenceValueModified) return false; return CanEditValue; } } /// /// Reverts the property value to reference value (if has). Handles undo. /// public void RevertToReferenceValue() { if (!Values.HasReferenceValue || !CanEditValue) return; RevertDiffToReference(); } /// /// Updates the reference value assigned to the editor's values container. Sends the event down the custom editors hierarchy to propagate the change. /// /// /// Has no effect on editors that don't have reference value assigned. /// public void RefreshReferenceValue() { if (!Values.HasReferenceValue) return; if (ParentEditor?.Values?.HasReferenceValue ?? false) { Values.RefreshReferenceValue(ParentEditor.Values.ReferenceValue); } for (int i = 0; i < ChildrenEditors.Count; i++) { ChildrenEditors[i].RefreshReferenceValue(); } } /// /// Clears all the reference value of the container in the whole custom editors tree (this container and all children). /// public void ClearReferenceValueAll() { Values?.ClearReferenceValue(); for (int i = 0; i < ChildrenEditors.Count; i++) { ChildrenEditors[i].ClearReferenceValueAll(); } } /// /// Copies the value to the system clipboard. /// public void Copy() { try { string text; var value = Values[0]; if (ParentEditor is Dedicated.ScriptsEditor) { // Script text = JsonSerializer.Serialize(value); // Remove properties that should be ignored when copy/pasting data if (text == null) text = string.Empty; int idx = text.IndexOf("\"Actor\":"); if (idx != -1) { int endIdx = text.IndexOf("\n", idx); if (endIdx != -1) text = text.Remove(idx, endIdx - idx); } idx = text.IndexOf("\"Parent\":"); if (idx != -1) { int endIdx = text.IndexOf("\n", idx); if (endIdx != -1) text = text.Remove(idx, endIdx - idx); } idx = text.IndexOf("\"OrderInParent\":"); if (idx != -1) { int endIdx = text.IndexOf("\n", idx); if (endIdx != -1) text = text.Remove(idx, endIdx - idx); } } else if (ScriptType.FlaxObject.IsAssignableFrom(Values.Type)) { // Object reference text = JsonSerializer.GetStringID(value as FlaxEngine.Object); } else { // Default text = JsonSerializer.Serialize(value, TypeUtils.GetType(Values.Type)); } Clipboard.Text = text; } catch (Exception ex) { Editor.LogWarning(ex); Editor.LogError("Cannot copy property. See log for more info."); } } private bool GetClipboardObject(out object result, bool deserialize) { result = null; var text = Clipboard.Text; if (string.IsNullOrEmpty(text)) return false; object obj; if (ParentEditor is Dedicated.ScriptsEditor) { // Script obj = Values[0]; if (deserialize) { if (Presenter.Undo != null && Presenter.Undo.Enabled) { using (new UndoBlock(Presenter.Undo, obj, "Paste values")) JsonSerializer.Deserialize(obj, text); } else { JsonSerializer.Deserialize(obj, text); } } else if (!text.StartsWith("{") || !text.EndsWith("}")) { return false; } } else if (ScriptType.FlaxObject.IsAssignableFrom(Values.Type)) { // Object reference if (text.Length != 32) return false; JsonSerializer.ParseID(text, out var id); obj = FlaxEngine.Object.Find(ref id); } else if (Color.TryParseHex(text, out var color)) { // Hex color obj = color; if (Values.Type.Type == typeof(Color32)) obj = (Color32)color; } else { // Default try { obj = Newtonsoft.Json.JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings); } catch { obj = null; } } if ((obj == null && !Values.Type.IsValueType) || Values.Type.IsInstanceOfType(obj)) { result = obj; return true; } return false; } /// /// Gets a value indicating whether can paste value from the system clipboard to the property value container. /// public bool CanPaste => !string.IsNullOrEmpty(Clipboard.Text); /// /// Sets the value from the system clipboard. /// public void Paste() { try { if (GetClipboardObject(out var obj, true)) { SetValue(obj); } } catch (Exception ex) { Editor.LogWarning(ex); Editor.LogError("Cannot paste property value. See log for more info."); } } private Actor FindActor(CustomEditor editor) { if (editor.Values[0] is Actor actor) return actor; if (editor.ParentEditor != null) return FindActor(editor.ParentEditor); return null; } private Actor FindPrefabRoot(CustomEditor editor) { if (editor.Values[0] is Actor actor) return FindPrefabRoot(actor); if (editor.ParentEditor != null) return FindPrefabRoot(editor.ParentEditor); return null; } private Actor FindPrefabRoot(Actor actor) { if (actor is Scene) return null; if (actor.IsPrefabRoot) return actor; return FindPrefabRoot(actor.Parent); } private SceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId, Actor endPoint) { var visited = new HashSet(); return FindObjectWithPrefabObjectId(actor, ref prefabObjectId, endPoint, visited); } private SceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId, Actor endPoint, HashSet visited) { if (visited.Contains(actor) || actor is Scene || actor == endPoint) return null; if (actor.PrefabObjectID == prefabObjectId) return actor; for (int i = 0; i < actor.ScriptsCount; i++) { var script = actor.GetScript(i); if (script != null && script.PrefabObjectID == prefabObjectId) return script; } for (int i = 0; i < actor.ChildrenCount; i++) { var child = actor.GetChild(i); if (child != null && child.PrefabObjectID == prefabObjectId) return child; } // Go up in the hierarchy return FindObjectWithPrefabObjectId(actor.Parent, ref prefabObjectId, endPoint, visited); } /// /// Sets the editor value to the default value (if assigned). /// public void SetValueToDefault() { SetValue(Utilities.Utils.CloneValue(Values.DefaultValue)); } /// /// Sets the editor value to the reference value (if assigned). /// public void SetValueToReference() { // Special case for object references // If prefab object has reference to other object in prefab needs to revert to matching prefab instance object not the reference prefab object value if (Values.ReferenceValue is SceneObject referenceSceneObject && referenceSceneObject.HasPrefabLink) { if (Values.Count > 1) { Editor.LogError("Cannot revert to reference value for more than one object selected."); return; } Actor prefabInstanceRoot = FindPrefabRoot(this); if (prefabInstanceRoot == null) { Editor.LogError("Cannot revert to reference value. Missing prefab instance root actor."); return; } var prefabObjectId = referenceSceneObject.PrefabObjectID; var prefabInstanceRef = FindObjectWithPrefabObjectId(FindActor(this), ref prefabObjectId, prefabInstanceRoot); if (prefabInstanceRef == null) { Editor.LogWarning("Missing prefab instance reference in the prefab instance. Cannot revert to it."); } SetValue(prefabInstanceRef); return; } SetValue(Utilities.Utils.CloneValue(Values.ReferenceValue)); } /// /// Sets the object value. Actual update is performed during editor refresh in sync. /// /// The value. /// The source editor token used by the value setter to support batching Undo actions (eg. for sliders or color pickers). protected void SetValue(object value, object token = null) { if (_isSetBlocked) return; if (OnDirty(this, value, token)) { _hasValueDirty = true; _valueToSet = value; } } /// /// Called when custom editor gets dirty (UI value has been modified). /// Allows to filter the event, block it or handle in a custom way. /// /// The editor. /// The value. /// The source editor token used by the value setter to support batching Undo actions (eg. for sliders or color pickers). /// True if allow to handle this event, otherwise false. protected virtual bool OnDirty(CustomEditor editor, object value, object token = null) { if (ParentEditor == null) return false; return ParentEditor.OnDirty(editor, value, token); } /// /// Called when custom editor sets the value to the object and resets the dirty state. Can be used to perform custom work after editing the target object. /// protected virtual void OnUnDirty() { } /// /// Clears the token assigned with parameter. Called on merged undo action end (eg. users stops using slider). /// protected virtual void ClearToken() { ParentEditor?.ClearToken(); } } }