// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.GUI; 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. /// /// protected class ItemInfo : IComparable { /// /// The member information from reflection. /// public ScriptMemberInfo Info; /// /// The order attribute. /// public EditorOrderAttribute Order; /// /// The display attribute. /// public EditorDisplayAttribute Display; /// /// The tooltip attribute. /// public TooltipAttribute Tooltip; /// /// 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 attribute. /// public VisibleIfAttribute VisibleIf; /// /// 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 a value indicating whether use dedicated group. /// public bool UseGroup => Display?.Group != null; /// /// 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 => Tooltip?.Text; /// /// 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); Tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); 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); VisibleIf = (VisibleIfAttribute)attributes.FirstOrDefault(x => x is VisibleIfAttribute); IsReadOnly = attributes.FirstOrDefault(x => x is ReadOnlyAttribute) != null; ExpandGroups = attributes.FirstOrDefault(x => x is ExpandGroupsAttribute) != null; IsReadOnly |= !info.HasSet; DisplayName = Display?.Name ?? CustomEditorsUtil.GetPropertyNameUI(info.Name); } /// /// 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); } // 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 Source; public PropertiesListElement PropertiesList; public bool Invert; public int LabelIndex; public bool GetValue(object instance) { var value = (bool)Source.GetValue(instance); if (Invert) value = !value; return value; } } private VisibleIfCache[] _visibleIfCaches; /// /// 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. /// The items. protected List GetItemsForType(ScriptType type, bool useProperties, bool useFields) { 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]; // Skip properties without getter or setter if (!p.HasGet || !p.HasSet) continue; var attributes = p.GetAttributes(true); // Skip hidden fields, handle special attributes if ((!p.IsPublic && !attributes.Any(x => x is ShowInEditorAttribute)) || 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 GetVisibleIfSource(ScriptType type, VisibleIfAttribute visibleIf) { var property = type.GetProperty(visibleIf.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 " + visibleIf.MemberName); return ScriptMemberInfo.Null; } if (property.ValueType.Type != typeof(bool)) { Debug.LogError("Invalid VisibleIf rule. Property has to return bool type " + visibleIf.MemberName); return ScriptMemberInfo.Null; } return property; } var field = type.GetField(visibleIf.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 " + visibleIf.MemberName); return ScriptMemberInfo.Null; } return field; } Debug.LogError("Invalid VisibleIf rule. Cannot find member " + visibleIf.MemberName); return ScriptMemberInfo.Null; } /// /// 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 = 0; if ((item.IsReadOnly || item.VisibleIf != null) && itemLayout.Children.Count > 0 && itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement propertiesListElement) { labelIndex = propertiesListElement.Labels.Count; } 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; child.Enabled = false; } } } if (item.VisibleIf != null) { PropertiesListElement list; if (itemLayout.Children.Count > 0 && itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement list1) { list = list1; } else { // TODO: support inlined objects hiding? return; } // Get source member used to check rule var sourceMember = GetVisibleIfSource(item.Info.DeclaringType, item.VisibleIf); if (sourceMember == ScriptType.Null) return; // Find the target control to show/hide // 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, Source = sourceMember, PropertiesList = list, LabelIndex = labelIndex, Invert = item.VisibleIf.Invert, }; } } /// 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", Height = ButtonSize, Width = ButtonSize, X = layout.ContainerControl.Width - ButtonSize - 4, AnchorPreset = AnchorPresets.MiddleRight, Parent = layout.ContainerControl }; button.Clicked += () => { var newType = Values.Type; SetValue(newType.CreateInstance()); RebuildLayoutOnRefresh(); }; } 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 GroupElement lastGroup = null; for (int i = 0; i < items.Count; i++) { var item = items[i]; // Check if use group LayoutElementsContainer itemLayout; if (item.UseGroup) { if (lastGroup == null || lastGroup.Panel.HeaderText != item.Display.Group) lastGroup = layout.Group(item.Display.Group); itemLayout = lastGroup; } else { lastGroup = null; itemLayout = layout; } // Space if (item.Space != null) itemLayout.Space(item.Space.Height); // Header if (item.Header != null) itemLayout.Header(item.Header.Text); 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); } } } /// public override void Refresh() { if (_visibleIfCaches != null) { try { for (int i = 0; i < _visibleIfCaches.Length; i++) { var c = _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.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; } } } } 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(); } } }