// 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();
}
}
}