Some checks failed
Build Android / Game (Android, Release ARM64) (push) Has been cancelled
Build iOS / Game (iOS, Release ARM64) (push) Has been cancelled
Build Linux / Editor (Linux, Development x64) (push) Has been cancelled
Build Linux / Game (Linux, Release x64) (push) Has been cancelled
Build macOS / Editor (Mac, Development ARM64) (push) Has been cancelled
Build macOS / Game (Mac, Release ARM64) (push) Has been cancelled
Build Windows / Editor (Windows, Development x64) (push) Has been cancelled
Build Windows / Game (Windows, Release x64) (push) Has been cancelled
Tests / Tests (Linux) (push) Has been cancelled
Tests / Tests (Windows) (push) Has been cancelled
Continuous Deployment / Editor (Windows) (push) Has been cancelled
Continuous Deployment / Game (Windows) (push) Has been cancelled
Continuous Deployment / Editor (Linux) (push) Has been cancelled
Continuous Deployment / Game (Linux) (push) Has been cancelled
Continuous Deployment / Editor (Mac) (push) Has been cancelled
Continuous Deployment / Game (Mac) (push) Has been cancelled
896 lines
34 KiB
C#
896 lines
34 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public class GenericEditor : CustomEditor
|
|
{
|
|
/// <summary>
|
|
/// Describes object property/field information for custom editors pipeline.
|
|
/// </summary>
|
|
/// <seealso cref="System.IComparable" />
|
|
public class ItemInfo : IComparable
|
|
{
|
|
private Options.GeneralOptions.MembersOrder _membersOrder;
|
|
|
|
/// <summary>
|
|
/// The member information from reflection.
|
|
/// </summary>
|
|
public ScriptMemberInfo Info;
|
|
|
|
/// <summary>
|
|
/// The order attribute.
|
|
/// </summary>
|
|
public EditorOrderAttribute Order;
|
|
|
|
/// <summary>
|
|
/// The display attribute.
|
|
/// </summary>
|
|
public EditorDisplayAttribute Display;
|
|
|
|
/// <summary>
|
|
/// The custom editor attribute.
|
|
/// </summary>
|
|
public CustomEditorAttribute CustomEditor;
|
|
|
|
/// <summary>
|
|
/// The custom editor alias attribute.
|
|
/// </summary>
|
|
public CustomEditorAliasAttribute CustomEditorAlias;
|
|
|
|
/// <summary>
|
|
/// The space attribute.
|
|
/// </summary>
|
|
public SpaceAttribute Space;
|
|
|
|
/// <summary>
|
|
/// The header attribute.
|
|
/// </summary>
|
|
public HeaderAttribute Header;
|
|
|
|
/// <summary>
|
|
/// The visible if attributes.
|
|
/// </summary>
|
|
public VisibleIfAttribute[] VisibleIfs;
|
|
|
|
/// <summary>
|
|
/// The read-only attribute usage flag.
|
|
/// </summary>
|
|
public bool IsReadOnly;
|
|
|
|
/// <summary>
|
|
/// The expand groups flag.
|
|
/// </summary>
|
|
public bool ExpandGroups;
|
|
|
|
/// <summary>
|
|
/// Gets the display name.
|
|
/// </summary>
|
|
public string DisplayName { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the overridden custom editor for item editing.
|
|
/// </summary>
|
|
public CustomEditor OverrideEditor
|
|
{
|
|
get
|
|
{
|
|
if (CustomEditor != null)
|
|
return (CustomEditor)Activator.CreateInstance(CustomEditor.Type);
|
|
if (CustomEditorAlias != null)
|
|
return (CustomEditor)TypeUtils.CreateInstance(CustomEditorAlias.TypeName);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the tooltip text (may be null if not provided).
|
|
/// </summary>
|
|
public string TooltipText;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ItemInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="info">The reflection information.</param>
|
|
public ItemInfo(ScriptMemberInfo info)
|
|
: this(info, info.GetAttributes(true))
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="ItemInfo"/> class.
|
|
/// </summary>
|
|
/// <param name="info">The reflection information.</param>
|
|
/// <param name="attributes">The attributes.</param>
|
|
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<VisibleIfAttribute>().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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the values.
|
|
/// </summary>
|
|
/// <param name="instanceValues">The instance values.</param>
|
|
/// <returns>The values container.</returns>
|
|
public ValueContainer GetValues(ValueContainer instanceValues)
|
|
{
|
|
return new ValueContainer(Info, instanceValues);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString()
|
|
{
|
|
return Info.Name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether can merge two item infos to present them at once.
|
|
/// </summary>
|
|
/// <param name="a">The a.</param>
|
|
/// <param name="b">The b.</param>
|
|
/// <returns><c>true</c> if can merge two item infos to present them at once; otherwise, <c>false</c>.</returns>
|
|
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<PropertiesList> _visibleIfPropertiesListsCache;
|
|
private static Stack<Dictionary<string, GroupElement>> _groups;
|
|
private static List<Dictionary<string, GroupElement>> _groupsPool;
|
|
private VisibleIfCache[] _visibleIfCaches;
|
|
private bool _isNull;
|
|
|
|
/// <summary>
|
|
/// Gets the items for the type
|
|
/// </summary>
|
|
/// <param name="type">The type.</param>
|
|
/// <returns>The items.</returns>
|
|
protected virtual List<ItemInfo> GetItemsForType(ScriptType type)
|
|
{
|
|
return GetItemsForType(type, type.IsClass, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the items for the type
|
|
/// </summary>
|
|
/// <param name="type">The type.</param>
|
|
/// <param name="useProperties">True if use type properties.</param>
|
|
/// <param name="useFields">True if use type fields.</param>
|
|
/// <param name="usePropertiesWithoutSetter">True if use type properties that have only getter method without setter method (aka read-only).</param>
|
|
/// <returns>The items.</returns>
|
|
public static List<ItemInfo> GetItemsForType(ScriptType type, bool useProperties, bool useFields, bool usePropertiesWithoutSetter = false)
|
|
{
|
|
var items = new List<ItemInfo>();
|
|
|
|
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<ScriptMemberInfo>();
|
|
|
|
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<string[]>(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<Dictionary<string, GroupElement>>();
|
|
if (_groupsPool == null)
|
|
_groupsPool = new List<Dictionary<string, GroupElement>>();
|
|
Dictionary<string, GroupElement> group;
|
|
if (_groupsPool.Count != 0)
|
|
{
|
|
group = _groupsPool[0];
|
|
_groupsPool.RemoveAt(0);
|
|
}
|
|
else
|
|
{
|
|
group = new Dictionary<string, GroupElement>();
|
|
}
|
|
_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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Evaluate the <see cref="VisibleIfAttribute"/> cache for a given property item.
|
|
/// </summary>
|
|
/// <param name="itemLayout">The item layout.</param>
|
|
/// <param name="item">The item.</param>
|
|
/// <param name="labelIndex">The label index.</param>
|
|
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(),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the label index.
|
|
/// </summary>
|
|
/// <param name="itemLayout">The item layout.</param>
|
|
/// <param name="item">The item.</param>
|
|
/// <returns>The label index.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns the property for the given item.
|
|
/// </summary>
|
|
/// <param name="itemLayout">The item layout.</param>
|
|
/// <param name="itemValues">The item values.</param>
|
|
/// <param name="item">The item.</param>
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
internal override void Initialize(CustomEditorPresenter presenter, LayoutElementsContainer layout, ValueContainer values)
|
|
{
|
|
_isNull = values != null && values.IsNull;
|
|
|
|
base.Initialize(presenter, layout, values);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Initialize(LayoutElementsContainer layout)
|
|
{
|
|
_visibleIfCaches = null;
|
|
|
|
// Collect items to edit
|
|
List<ItemInfo> 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("<null>");
|
|
return;
|
|
}
|
|
|
|
items = GetItemsForType(TypeUtils.GetObjectType(value));
|
|
}
|
|
else
|
|
{
|
|
var types = ValuesTypes;
|
|
items = new List<ItemInfo>(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();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Deinitialize()
|
|
{
|
|
_visibleIfCaches = null;
|
|
_visibleIfPropertiesListsCache = null;
|
|
|
|
base.Deinitialize();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<PropertiesList>();
|
|
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();
|
|
}
|
|
}
|
|
}
|