// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors.Editors { /// /// Default implementation of the inspector used when no specified inspector is provided for the type. Inspector /// displays GUI for all the inspectable fields in the object. /// public class GenericEditor : CustomEditor { /// /// Describes object property/field information for custom editors pipeline. /// /// public class ItemInfo : IComparable { private Options.GeneralOptions.MembersOrder _membersOrder; /// /// The member information from reflection. /// public ScriptMemberInfo Info; /// /// The order attribute. /// public EditorOrderAttribute Order; /// /// The display attribute. /// public EditorDisplayAttribute Display; /// /// The custom editor attribute. /// public CustomEditorAttribute CustomEditor; /// /// The custom editor alias attribute. /// public CustomEditorAliasAttribute CustomEditorAlias; /// /// The space attribute. /// public SpaceAttribute Space; /// /// The header attribute. /// public HeaderAttribute Header; /// /// The visible if attributes. /// public VisibleIfAttribute[] VisibleIfs; /// /// The read-only attribute usage flag. /// public bool IsReadOnly; /// /// The expand groups flag. /// public bool ExpandGroups; /// /// Gets the display name. /// public string DisplayName { get; } /// /// Gets the overridden custom editor for item editing. /// public CustomEditor OverrideEditor { get { if (CustomEditor != null) return (CustomEditor)Activator.CreateInstance(CustomEditor.Type); if (CustomEditorAlias != null) return (CustomEditor)TypeUtils.CreateInstance(CustomEditorAlias.TypeName); return null; } } /// /// Gets the tooltip text (may be null if not provided). /// public string TooltipText; /// /// Initializes a new instance of the class. /// /// The reflection information. public ItemInfo(ScriptMemberInfo info) : this(info, info.GetAttributes(true)) { } /// /// Initializes a new instance of the class. /// /// The reflection information. /// The attributes. public ItemInfo(ScriptMemberInfo info, object[] attributes) { Info = info; Order = (EditorOrderAttribute)attributes.FirstOrDefault(x => x is EditorOrderAttribute); Display = (EditorDisplayAttribute)attributes.FirstOrDefault(x => x is EditorDisplayAttribute); CustomEditor = (CustomEditorAttribute)attributes.FirstOrDefault(x => x is CustomEditorAttribute); CustomEditorAlias = (CustomEditorAliasAttribute)attributes.FirstOrDefault(x => x is CustomEditorAliasAttribute); Space = (SpaceAttribute)attributes.FirstOrDefault(x => x is SpaceAttribute); Header = (HeaderAttribute)attributes.FirstOrDefault(x => x is HeaderAttribute); VisibleIfs = attributes.OfType().ToArray(); IsReadOnly = attributes.FirstOrDefault(x => x is ReadOnlyAttribute) != null; ExpandGroups = attributes.FirstOrDefault(x => x is ExpandGroupsAttribute) != null; IsReadOnly |= !info.HasSet; DisplayName = Display?.Name ?? Utilities.Utils.GetPropertyNameUI(info.Name); var editor = Editor.Instance; TooltipText = editor.CodeDocs.GetTooltip(info, attributes); _membersOrder = editor.Options.Options.General.ScriptMembersOrder; } /// /// Gets the values. /// /// The instance values. /// The values container. public ValueContainer GetValues(ValueContainer instanceValues) { return new ValueContainer(Info, instanceValues); } /// public int CompareTo(object obj) { if (obj is ItemInfo other) { // By order if (Order != null) { if (other.Order != null) return Order.Order - other.Order.Order; return -1; } if (other.Order != null) return 1; // By group name if (Display?.Group != null) { if (other.Display?.Group != null) return string.Compare(Display.Group, other.Display.Group, StringComparison.InvariantCulture); } if (_membersOrder == Options.GeneralOptions.MembersOrder.Declaration) { // By declaration order if (Info.MetadataToken > other.Info.MetadataToken) return 1; if (Info.MetadataToken < other.Info.MetadataToken) return -1; } // By name return string.Compare(Info.Name, other.Info.Name, StringComparison.InvariantCulture); } return 0; } /// public override string ToString() { return Info.Name; } /// /// Determines whether can merge two item infos to present them at once. /// /// The a. /// The b. /// true if can merge two item infos to present them at once; otherwise, false. public static bool CanMerge(ItemInfo a, ItemInfo b) { if (a.Info.DeclaringType != b.Info.DeclaringType) return false; return a.Info.Name == b.Info.Name; } } private struct VisibleIfCache { public ScriptMemberInfo Target; public ScriptMemberInfo[] Sources; public PropertiesListElement PropertiesList; public GroupElement Group; public bool[] InversionList; public int LabelIndex; public bool GetValue(object instance) { bool value = true; for (int i = 0; i < Sources.Length; i++) { bool currentValue = (bool)Sources[i].GetValue(instance); if (InversionList[i]) currentValue = !currentValue; value = value && currentValue; } return value; } } private static HashSet _visibleIfPropertiesListsCache; private static Stack> _groups; private static List> _groupsPool; private VisibleIfCache[] _visibleIfCaches; private bool _isNull; /// /// Gets the items for the type /// /// The type. /// The items. protected virtual List GetItemsForType(ScriptType type) { return GetItemsForType(type, type.IsClass, true); } /// /// Gets the items for the type /// /// The type. /// True if use type properties. /// True if use type fields. /// True if use type properties that have only getter method without setter method (aka read-only). /// The items. public static List GetItemsForType(ScriptType type, bool useProperties, bool useFields, bool usePropertiesWithoutSetter = false) { var items = new List(); if (useProperties) { // Process properties var properties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); items.Capacity = Math.Max(items.Capacity, items.Count + properties.Length); for (int i = 0; i < properties.Length; i++) { var p = properties[i]; var attributes = p.GetAttributes(true); var showInEditor = attributes.Any(x => x is ShowInEditorAttribute); // Skip properties without getter or setter if (!p.HasGet || (!p.HasSet && !showInEditor && !usePropertiesWithoutSetter)) continue; // Skip hidden fields, handle special attributes if ((!p.IsPublic && !showInEditor) || attributes.Any(x => x is HideInEditorAttribute)) continue; items.Add(new ItemInfo(p, attributes)); } } if (useFields) { // Process fields var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); items.Capacity = Math.Max(items.Capacity, items.Count + fields.Length); for (int i = 0; i < fields.Length; i++) { var f = fields[i]; var attributes = f.GetAttributes(true); // Skip hidden fields, handle special attributes if ((!f.IsPublic && !attributes.Any(x => x is ShowInEditorAttribute)) || attributes.Any(x => x is HideInEditorAttribute)) continue; items.Add(new ItemInfo(f, attributes)); } } return items; } private static ScriptMemberInfo[] GetVisibleIfSources(ScriptType type, VisibleIfAttribute[] visibleIfs) { ScriptMemberInfo[] members = Array.Empty(); for (int i = 0; i < visibleIfs.Length; i++) { var property = type.GetProperty(visibleIfs[i].MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); if (property != ScriptMemberInfo.Null) { if (!property.HasGet) { Debug.LogError("Invalid VisibleIf rule. Property has missing getter " + visibleIfs[i].MemberName); continue; } if (property.ValueType.Type != typeof(bool)) { Debug.LogError("Invalid VisibleIf rule. Property has to return bool type " + visibleIfs[i].MemberName); continue; } members = members.Append(property).ToArray(); continue; } var field = type.GetField(visibleIfs[i].MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); if (field != ScriptMemberInfo.Null) { if (field.ValueType.Type != typeof(bool)) { Debug.LogError("Invalid VisibleIf rule. Field has to be bool type " + visibleIfs[i].MemberName); continue; } members = members.Append(field).ToArray(); continue; } Debug.LogError("Invalid VisibleIf rule. Cannot find member " + visibleIfs[i].MemberName); } return members; } private static void GroupPanelCheckIfCanRevert(LayoutElementsContainer layout, ref bool canRevertReference, ref bool canRevertDefault) { if (layout == null || canRevertReference && canRevertDefault) return; foreach (var editor in layout.Editors) { canRevertReference |= editor.CanRevertReferenceValue; canRevertDefault |= editor.CanRevertDefaultValue; } foreach (var child in layout.Children) GroupPanelCheckIfCanRevert(child as LayoutElementsContainer, ref canRevertReference, ref canRevertDefault); } private static void OnGroupPanelRevert(LayoutElementsContainer layout, bool toDefault) { if (layout == null) return; foreach (var editor in layout.Editors) { if (toDefault && editor.CanRevertDefaultValue) editor.RevertToDefaultValue(); else if (!toDefault && editor.CanRevertReferenceValue) editor.RevertToReferenceValue(); } foreach (var child in layout.Children) OnGroupPanelRevert(child as LayoutElementsContainer, toDefault); } private static void OnGroupPanelCopy(LayoutElementsContainer layout) { if (layout.Editors.Count == 1) { layout.Editors[0].Copy(); } else if (layout.Editors.Count != 0) { var data = new string[layout.Editors.Count]; var sb = new StringBuilder(); sb.Append("[\n"); for (var i = 0; i < layout.Editors.Count; i++) { layout.Editors[i].Copy(); if (i != 0) sb.Append(",\n"); sb.Append(Clipboard.Text); data[i] = Clipboard.Text; } sb.Append("\n]"); Clipboard.Text = sb.ToString(); Clipboard.Text = JsonSerializer.Serialize(data); } else if (layout.Children.Any(x => x is LayoutElementsContainer)) { foreach (var child in layout.Children) { if (child is LayoutElementsContainer childContainer) { OnGroupPanelCopy(childContainer); break; } } } } private static bool OnGroupPanelCanCopy(LayoutElementsContainer layout) { return layout.Editors.Count != 0 || layout.Children.Any(x => x is LayoutElementsContainer); } private static void OnGroupPanelPaste(LayoutElementsContainer layout) { if (layout.Editors.Count == 1) { layout.Editors[0].Paste(); } else if (layout.Editors.Count != 0) { var sb = Clipboard.Text; if (!string.IsNullOrEmpty(sb)) { try { var data = JsonSerializer.Deserialize(sb); if (data == null || data.Length != layout.Editors.Count) return; for (var i = 0; i < layout.Editors.Count; i++) { Clipboard.Text = data[i]; layout.Editors[i].Paste(); } } catch { } finally { Clipboard.Text = sb; } } } else if (layout.Children.Any(x => x is LayoutElementsContainer)) { foreach (var child in layout.Children) { if (child is LayoutElementsContainer childContainer) { OnGroupPanelPaste(childContainer); break; } } } } private static bool OnGroupPanelCanPaste(LayoutElementsContainer layout) { if (layout.Editors.Count == 1) { return layout.Editors[0].CanPaste; } if (layout.Editors.Count != 0) { return !string.IsNullOrEmpty(Clipboard.Text); } if (layout.Children.Any(x => x is LayoutElementsContainer)) { foreach (var child in layout.Children) { if (child is LayoutElementsContainer childContainer) return OnGroupPanelCanPaste(childContainer); } } return false; } private static void OnGroupPanelMouseButtonRightClicked(DropPanel groupPanel, Float2 location) { var group = (GroupElement)groupPanel.Tag; bool canRevertReference = false, canRevertDefault = false; GroupPanelCheckIfCanRevert(group, ref canRevertReference, ref canRevertDefault); var menu = new ContextMenu(); var revertToPrefab = menu.AddButton("Revert to Prefab", () => OnGroupPanelRevert(group, false)); revertToPrefab.Enabled = canRevertReference; var resetToDefault = menu.AddButton("Reset to default", () => OnGroupPanelRevert(group, true)); resetToDefault.Enabled = canRevertDefault; menu.AddSeparator(); var copy = menu.AddButton("Copy", () => OnGroupPanelCopy(group)); copy.Enabled = OnGroupPanelCanCopy(group); var paste = menu.AddButton("Paste", () => OnGroupPanelPaste(group)); paste.Enabled = OnGroupPanelCanPaste(group); menu.Show(groupPanel, location); } internal static void OnGroupsBegin() { if (_groups == null) _groups = new Stack>(); if (_groupsPool == null) _groupsPool = new List>(); Dictionary group; if (_groupsPool.Count != 0) { group = _groupsPool[0]; _groupsPool.RemoveAt(0); } else { group = new Dictionary(); } _groups.Push(group); } internal static void OnGroupsEnd() { var groups = _groups.Pop(); groups.Clear(); _groupsPool.Add(groups); } internal static LayoutElementsContainer OnGroup(LayoutElementsContainer layout, EditorDisplayAttribute display) { if (display?.Group != null) { var groups = _groups.Peek(); if (groups.TryGetValue(display.Group, out var group)) { // Reuse group layout = group; } else { // Add new group group = layout.Group(display.Group); group.Panel.Tag = group; group.Panel.MouseButtonRightClicked += OnGroupPanelMouseButtonRightClicked; groups.Add(display.Group, group); layout = group; } } return layout; } /// /// Evaluate the cache for a given property item. /// /// The item layout. /// The item. /// The label index. protected virtual void EvaluateVisibleIf(LayoutElementsContainer itemLayout, ItemInfo item, int labelIndex) { if (item.VisibleIfs.Length > 0 && itemLayout.Children.Count > 0) { PropertiesListElement list = null; GroupElement group = null; if (itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement list1) list = list1; else if (itemLayout.Children[itemLayout.Children.Count - 1] is GroupElement group1) group = group1; else return; // Get source member used to check rule var sourceMembers = GetVisibleIfSources(item.Info.DeclaringType, item.VisibleIfs); if (sourceMembers.Length == 0) return; // Resize cache if (_visibleIfCaches == null) _visibleIfCaches = new VisibleIfCache[8]; int count = 0; while (count < _visibleIfCaches.Length && _visibleIfCaches[count].Target != ScriptType.Null) count++; if (count >= _visibleIfCaches.Length) Array.Resize(ref _visibleIfCaches, count * 2); // Add item _visibleIfCaches[count] = new VisibleIfCache { Target = item.Info, Sources = sourceMembers, PropertiesList = list, Group = group, LabelIndex = labelIndex, InversionList = item.VisibleIfs.Select((x, i) => x.Invert).ToArray(), }; } } /// /// Get the label index. /// /// The item layout. /// The item. /// The label index. protected virtual int GetLabelIndex(LayoutElementsContainer itemLayout, ItemInfo item) { int labelIndex = 0; if ((item.IsReadOnly || item.VisibleIfs.Length > 0) && itemLayout.Children.Count > 0 && itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement propertiesListElement) { labelIndex = propertiesListElement.Labels.Count; } return labelIndex; } /// /// Spawns the property for the given item. /// /// The item layout. /// The item values. /// The item. protected virtual void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item) { int labelIndex = GetLabelIndex(itemLayout, item); itemLayout.Property(item.DisplayName, itemValues, item.OverrideEditor, item.TooltipText); if (item.IsReadOnly && itemLayout.Children.Count > 0) { PropertiesListElement list = null; int firstChildControlIndex = 0; bool disableSingle = true; var control = itemLayout.Children[itemLayout.Children.Count - 1]; if (control is GroupElement group && group.Children.Count > 0) { list = group.Children[0] as PropertiesListElement; disableSingle = false; // Disable all nested editors } else if (control is PropertiesListElement list1) { list = list1; firstChildControlIndex = list.Labels[labelIndex].FirstChildControlIndex; } if (list != null) { // Disable controls added to the editor var count = list.Properties.Children.Count; for (int j = firstChildControlIndex; j < count; j++) { var child = list.Properties.Children[j]; if (disableSingle && child is PropertyNameLabel) break; if (child != null) child.Enabled = false; } } } EvaluateVisibleIf(itemLayout, item, labelIndex); } /// internal override void Initialize(CustomEditorPresenter presenter, LayoutElementsContainer layout, ValueContainer values) { _isNull = values != null && values.IsNull; base.Initialize(presenter, layout, values); } /// public override void Initialize(LayoutElementsContainer layout) { _visibleIfCaches = null; // Collect items to edit List items; if (!HasDifferentTypes) { var value = Values[0]; if (value == null) { // Check if it's an object type that can be created in editor var type = Values.Type; if (type != ScriptMemberInfo.Null && type.CanCreateInstance) { layout = layout.Space(20); const float ButtonSize = 14.0f; var button = new Button { Text = "+", TooltipText = "Create a new instance of the object", Size = new Float2(ButtonSize, ButtonSize), AnchorPreset = AnchorPresets.MiddleRight, Parent = layout.ContainerControl, Location = new Float2(layout.ContainerControl.Width - ButtonSize - 4, (layout.ContainerControl.Height - ButtonSize) * 0.5f), }; button.Clicked += () => SetValue(Values.Type.CreateInstance()); } layout.Label(""); return; } items = GetItemsForType(TypeUtils.GetObjectType(value)); } else { var types = ValuesTypes; items = new List(GetItemsForType(types[0])); for (int i = 1; i < types.Length && items.Count > 0; i++) { var otherItems = GetItemsForType(types[i]); // Merge items for (int j = 0; j < items.Count && items.Count > 0; j++) { bool isInvalid = true; for (int k = 0; k < otherItems.Count; k++) { var a = items[j]; var b = otherItems[k]; if (ItemInfo.CanMerge(a, b)) { isInvalid = false; break; } } if (isInvalid) { items.RemoveAt(j--); } } } } // Sort items items.Sort(); // Add items OnGroupsBegin(); for (int i = 0; i < items.Count; i++) { var item = items[i]; // Group var itemLayout = OnGroup(layout, item.Display); // Space if (item.Space != null) itemLayout.Space(item.Space.Height); // Header if (item.Header != null) itemLayout.Header(item.Header); try { // Peek values ValueContainer itemValues = item.GetValues(Values); // Spawn property editor SpawnProperty(itemLayout, itemValues, item); } catch (Exception ex) { Editor.LogWarning("Failed to setup values and UI for item " + item); Editor.LogWarning(ex.Message); Editor.LogWarning(ex.StackTrace); return; } // Expand all parent groups if need to if (item.ExpandGroups) { var c = itemLayout.ContainerControl; do { if (c is DropPanel dropPanel) dropPanel.Open(false); else if (c is CustomEditorPresenter.PresenterPanel) break; c = c.Parent; } while (c != null); } } OnGroupsEnd(); } /// protected override void Deinitialize() { _visibleIfCaches = null; _visibleIfPropertiesListsCache = null; base.Deinitialize(); } /// public override void Refresh() { // Automatic refresh when value nullability changed if (_isNull != Values.IsNull) { RebuildLayout(); return; } if (_visibleIfCaches != null) { if (_visibleIfPropertiesListsCache == null) _visibleIfPropertiesListsCache = new HashSet(); else _visibleIfPropertiesListsCache.Clear(); try { // Update VisibleIf rules for (int i = 0; i < _visibleIfCaches.Length; i++) { ref var c = ref _visibleIfCaches[i]; if (c.Target == ScriptMemberInfo.Null) break; // Check rule (all objects must allow to show this property) bool visible = true; for (int j = 0; j < Values.Count; j++) { if (Values[j] != null && !c.GetValue(Values[j])) { visible = false; break; } } // Apply the visibility (note: there may be no label) if (c.LabelIndex != -1 && c.PropertiesList != null && c.PropertiesList.Labels.Count > c.LabelIndex) { var label = c.PropertiesList.Labels[c.LabelIndex]; label.Visible = visible; for (int j = label.FirstChildControlIndex; j < c.PropertiesList.Properties.Children.Count; j++) { var child = c.PropertiesList.Properties.Children[j]; if (child is PropertyNameLabel) break; child.Visible = visible; } } if (c.Group != null) { c.Group.Panel.Visible = visible; } if (c.PropertiesList != null) _visibleIfPropertiesListsCache.Add(c.PropertiesList.Properties); } // Hide properties lists with all labels being hidden foreach (var propertiesList in _visibleIfPropertiesListsCache) { propertiesList.Visible = propertiesList.Children.Any(c => c.Visible); } // Hide group panels with all properties lists hidden foreach (var propertiesList in _visibleIfPropertiesListsCache) { if (propertiesList.Parent is DropPanel dropPanel) dropPanel.Visible = propertiesList.Visible || !dropPanel.Children.All(c => c is PropertiesList && !c.Visible); } } catch (Exception ex) { Editor.LogWarning(ex); Editor.LogError("Failed to update VisibleIf rules. " + ex.Message); // Remove rules to prevent error in loop _visibleIfCaches = null; } } base.Refresh(); } } }