mirror of
https://github.com/GrahamKracker/UnityExplorer.git
synced 2025-07-01 19:13:03 +08:00
376 lines
14 KiB
C#
376 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
using UnityEngine.UI;
|
|
using UnityExplorer.Helpers;
|
|
using UnityExplorer.UI;
|
|
|
|
namespace UnityExplorer.Inspectors.Reflection
|
|
{
|
|
public class InteractiveValue
|
|
{
|
|
/// <summary>
|
|
/// Get the <see cref="InteractiveValue"/> subclass which supports the provided <paramref name="type"/>.
|
|
/// </summary>
|
|
/// <param name="type">The <see cref="Type"/> which you want the <see cref="InteractiveValue"/> Type for.</param>
|
|
/// <returns>The best subclass of <see cref="InteractiveValue"/> which supports the provided <paramref name="type"/>.</returns>
|
|
public static Type GetIValueForType(Type type)
|
|
{
|
|
// rather ugly but I couldn't think of a cleaner way that was worth it.
|
|
// switch-case doesn't really work here.
|
|
|
|
// arbitrarily check some types, fastest methods first.
|
|
if (type == typeof(bool))
|
|
return typeof(InteractiveBool);
|
|
// if type is primitive then it must be a number if its not a bool
|
|
else if (type.IsPrimitive)
|
|
return typeof(InteractiveNumber);
|
|
// check for strings
|
|
else if (type == typeof(string))
|
|
return typeof(InteractiveString);
|
|
// check for enum/flags
|
|
else if (typeof(Enum).IsAssignableFrom(type))
|
|
{
|
|
// NET 3.5 doesn't have "GetCustomAttribute", gotta use the multiple version.
|
|
if (type.GetCustomAttributes(typeof(FlagsAttribute), true) is object[] fa && fa.Any())
|
|
return typeof(InteractiveFlags);
|
|
else
|
|
return typeof(InteractiveEnum);
|
|
}
|
|
// check for unity struct types
|
|
else if (InteractiveUnityStruct.SupportsType(type))
|
|
return typeof(InteractiveUnityStruct);
|
|
// check Transform, force InteractiveValue so they dont become InteractiveEnumerables.
|
|
else if (typeof(Transform).IsAssignableFrom(type))
|
|
return typeof(InteractiveValue);
|
|
// check Dictionaries before Enumerables
|
|
else if (ReflectionHelpers.IsDictionary(type))
|
|
return typeof(InteractiveDictionary);
|
|
// finally check for Enumerables
|
|
else if (ReflectionHelpers.IsEnumerable(type))
|
|
return typeof(InteractiveEnumerable);
|
|
// fallback to default
|
|
else
|
|
return typeof(InteractiveValue);
|
|
}
|
|
|
|
public static InteractiveValue Create(object value, Type fallbackType)
|
|
{
|
|
var type = ReflectionHelpers.GetActualType(value) ?? fallbackType;
|
|
var iType = GetIValueForType(type);
|
|
|
|
return (InteractiveValue)Activator.CreateInstance(iType, new object[] { value, type });
|
|
}
|
|
|
|
// ~~~~~~~~~ Instance ~~~~~~~~~
|
|
|
|
public InteractiveValue(object value, Type valueType)
|
|
{
|
|
this.Value = value;
|
|
this.FallbackType = valueType;
|
|
}
|
|
|
|
public CacheObjectBase Owner;
|
|
|
|
public object Value;
|
|
public readonly Type FallbackType;
|
|
|
|
public virtual bool HasSubContent => false;
|
|
public virtual bool SubContentWanted => false;
|
|
public virtual bool WantInspectBtn => true;
|
|
|
|
public string DefaultLabel => m_defaultLabel ?? GetDefaultLabel();
|
|
internal string m_defaultLabel;
|
|
internal string m_richValueType;
|
|
|
|
public bool m_UIConstructed;
|
|
|
|
public virtual void OnDestroy()
|
|
{
|
|
if (this.m_valueContent)
|
|
{
|
|
m_valueContent.transform.SetParent(null, false);
|
|
m_valueContent.SetActive(false);
|
|
GameObject.Destroy(this.m_valueContent.gameObject);
|
|
}
|
|
|
|
DestroySubContent();
|
|
}
|
|
|
|
public virtual void DestroySubContent()
|
|
{
|
|
if (this.m_subContentParent && HasSubContent)
|
|
{
|
|
for (int i = 0; i < this.m_subContentParent.transform.childCount; i++)
|
|
{
|
|
var child = m_subContentParent.transform.GetChild(i);
|
|
if (child)
|
|
GameObject.Destroy(child.gameObject);
|
|
}
|
|
}
|
|
|
|
m_subContentConstructed = false;
|
|
}
|
|
|
|
public virtual void OnValueUpdated()
|
|
{
|
|
if (!m_UIConstructed)
|
|
ConstructUI(m_mainContentParent, m_subContentParent);
|
|
|
|
if (Owner is CacheMember ownerMember && !string.IsNullOrEmpty(ownerMember.ReflectionException))
|
|
OnException(ownerMember);
|
|
else
|
|
RefreshUIForValue();
|
|
}
|
|
|
|
public virtual void OnException(CacheMember member)
|
|
{
|
|
if (m_UIConstructed)
|
|
m_baseLabel.text = "<color=red>" + member.ReflectionException + "</color>";
|
|
|
|
Value = null;
|
|
}
|
|
|
|
public virtual void RefreshUIForValue()
|
|
{
|
|
GetDefaultLabel();
|
|
m_baseLabel.text = DefaultLabel;
|
|
}
|
|
|
|
public void RefreshElementsAfterUpdate()
|
|
{
|
|
if (WantInspectBtn)
|
|
{
|
|
bool shouldShowInspect = !Value.IsNullOrDestroyed();
|
|
|
|
if (m_inspectButton.activeSelf != shouldShowInspect)
|
|
m_inspectButton.SetActive(shouldShowInspect);
|
|
}
|
|
|
|
bool subContentWanted = SubContentWanted;
|
|
if (Owner is CacheMember cm && (!cm.HasEvaluated || !string.IsNullOrEmpty(cm.ReflectionException)))
|
|
subContentWanted = false;
|
|
|
|
if (HasSubContent)
|
|
{
|
|
if (m_subExpandBtn.gameObject.activeSelf != subContentWanted)
|
|
m_subExpandBtn.gameObject.SetActive(subContentWanted);
|
|
|
|
if (!subContentWanted && m_subContentParent.activeSelf)
|
|
ToggleSubcontent();
|
|
}
|
|
}
|
|
|
|
public virtual void ConstructSubcontent()
|
|
{
|
|
m_subContentConstructed = true;
|
|
}
|
|
|
|
public void ToggleSubcontent()
|
|
{
|
|
if (!this.m_subContentParent.activeSelf)
|
|
{
|
|
this.m_subContentParent.SetActive(true);
|
|
this.m_subContentParent.transform.SetAsLastSibling();
|
|
m_subExpandBtn.GetComponentInChildren<Text>().text = "▼";
|
|
}
|
|
else
|
|
{
|
|
this.m_subContentParent.SetActive(false);
|
|
m_subExpandBtn.GetComponentInChildren<Text>().text = "▲";
|
|
}
|
|
|
|
OnToggleSubcontent(m_subContentParent.activeSelf);
|
|
|
|
RefreshElementsAfterUpdate();
|
|
}
|
|
|
|
internal virtual void OnToggleSubcontent(bool toggle)
|
|
{
|
|
if (!m_subContentConstructed)
|
|
ConstructSubcontent();
|
|
}
|
|
|
|
internal MethodInfo m_toStringMethod;
|
|
internal MethodInfo m_toStringFormatMethod;
|
|
internal bool m_gotToStringMethods;
|
|
|
|
public string GetDefaultLabel(bool updateType = true)
|
|
{
|
|
var valueType = Value?.GetType() ?? this.FallbackType;
|
|
if (updateType)
|
|
m_richValueType = UISyntaxHighlight.ParseFullSyntax(valueType, true);
|
|
|
|
if (!Owner.HasEvaluated)
|
|
return m_defaultLabel = $"<i><color=grey>Not yet evaluated</color> ({m_richValueType})</i>";
|
|
|
|
if (Value.IsNullOrDestroyed())
|
|
return m_defaultLabel = $"<color=grey>null</color> ({m_richValueType})";
|
|
|
|
string label;
|
|
|
|
// Two dirty fixes for TextAsset and EventSystem, which can have very long ToString results.
|
|
if (Value is TextAsset textAsset)
|
|
{
|
|
label = textAsset.text;
|
|
|
|
if (label.Length > 10)
|
|
label = $"{label.Substring(0, 10)}...";
|
|
|
|
label = $"\"{label}\" {textAsset.name} ({m_richValueType})";
|
|
}
|
|
else if (Value is EventSystem)
|
|
{
|
|
label = m_richValueType;
|
|
}
|
|
else // For everything else...
|
|
{
|
|
if (!m_gotToStringMethods)
|
|
{
|
|
m_gotToStringMethods = true;
|
|
|
|
m_toStringMethod = valueType.GetMethod("ToString", new Type[0]);
|
|
m_toStringFormatMethod = valueType.GetMethod("ToString", new Type[] { typeof(string) });
|
|
|
|
// test format method actually works
|
|
try
|
|
{
|
|
m_toStringFormatMethod.Invoke(Value, new object[] { "F3" });
|
|
}
|
|
catch
|
|
{
|
|
m_toStringFormatMethod = null;
|
|
}
|
|
}
|
|
|
|
string toString;
|
|
if (m_toStringFormatMethod != null)
|
|
toString = (string)m_toStringFormatMethod.Invoke(Value, new object[] { "F3" });
|
|
else
|
|
toString = (string)m_toStringMethod.Invoke(Value, new object[0]);
|
|
|
|
string typeName = valueType.FullName;
|
|
if (typeName.StartsWith("Il2CppSystem."))
|
|
typeName = typeName.Substring(6, typeName.Length - 6);
|
|
#if CPP
|
|
var cppType = UnhollowerRuntimeLib.Il2CppType.From(valueType);
|
|
if (cppType != null && ReflectionHelpers.UnobfuscatedTypeNames.ContainsKey(cppType.FullName))
|
|
{
|
|
typeName = ReflectionHelpers.UnobfuscatedTypeNames[cppType.FullName];
|
|
toString = toString.Replace(cppType.FullName, typeName);
|
|
}
|
|
#endif
|
|
|
|
// If the ToString is just the type name, use our syntax highlighted type name instead.
|
|
if (toString == typeName)
|
|
{
|
|
label = m_richValueType;
|
|
}
|
|
else // Otherwise, parse the result and put our highlighted name in.
|
|
{
|
|
if (toString.Length > 200)
|
|
toString = toString.Substring(0, 200) + "...";
|
|
|
|
label = toString;
|
|
|
|
var unityType = $"({valueType.FullName})";
|
|
if (Value is UnityEngine.Object && label.Contains(unityType))
|
|
label = label.Replace(unityType, $"({m_richValueType})");
|
|
else
|
|
label += $" ({m_richValueType})";
|
|
}
|
|
}
|
|
|
|
return m_defaultLabel = label;
|
|
}
|
|
|
|
#region UI CONSTRUCTION
|
|
|
|
internal GameObject m_mainContentParent;
|
|
internal GameObject m_subContentParent;
|
|
|
|
internal GameObject m_valueContent;
|
|
internal GameObject m_inspectButton;
|
|
internal Text m_baseLabel;
|
|
|
|
internal Button m_subExpandBtn;
|
|
internal bool m_subContentConstructed;
|
|
|
|
public virtual void ConstructUI(GameObject parent, GameObject subGroup)
|
|
{
|
|
m_UIConstructed = true;
|
|
|
|
m_valueContent = UIFactory.CreateHorizontalGroup(parent, new Color(1, 1, 1, 0));
|
|
m_valueContent.name = "InteractiveValue.ValueContent";
|
|
var mainRect = m_valueContent.GetComponent<RectTransform>();
|
|
mainRect.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 25);
|
|
var mainGroup = m_valueContent.GetComponent<HorizontalLayoutGroup>();
|
|
mainGroup.childForceExpandWidth = false;
|
|
mainGroup.childControlWidth = true;
|
|
mainGroup.childForceExpandHeight = false;
|
|
mainGroup.childControlHeight = true;
|
|
mainGroup.spacing = 4;
|
|
mainGroup.childAlignment = TextAnchor.UpperLeft;
|
|
var mainLayout = m_valueContent.AddComponent<LayoutElement>();
|
|
mainLayout.flexibleWidth = 9000;
|
|
mainLayout.minWidth = 175;
|
|
mainLayout.minHeight = 25;
|
|
mainLayout.flexibleHeight = 0;
|
|
|
|
// subcontent expand button TODO
|
|
if (HasSubContent)
|
|
{
|
|
var subBtnObj = UIFactory.CreateButton(m_valueContent, new Color(0.3f, 0.3f, 0.3f));
|
|
var btnLayout = subBtnObj.AddComponent<LayoutElement>();
|
|
btnLayout.minHeight = 25;
|
|
btnLayout.minWidth = 25;
|
|
btnLayout.flexibleWidth = 0;
|
|
btnLayout.flexibleHeight = 0;
|
|
var btnText = subBtnObj.GetComponentInChildren<Text>();
|
|
btnText.text = "▲";
|
|
m_subExpandBtn = subBtnObj.GetComponent<Button>();
|
|
m_subExpandBtn.onClick.AddListener(() =>
|
|
{
|
|
ToggleSubcontent();
|
|
});
|
|
}
|
|
|
|
// inspect button
|
|
|
|
m_inspectButton = UIFactory.CreateButton(m_valueContent, new Color(0.3f, 0.3f, 0.3f, 0.2f));
|
|
var inspectLayout = m_inspectButton.AddComponent<LayoutElement>();
|
|
inspectLayout.minWidth = 60;
|
|
inspectLayout.minHeight = 25;
|
|
inspectLayout.flexibleHeight = 0;
|
|
inspectLayout.flexibleWidth = 0;
|
|
var inspectText = m_inspectButton.GetComponentInChildren<Text>();
|
|
inspectText.text = "Inspect";
|
|
var inspectBtn = m_inspectButton.GetComponent<Button>();
|
|
|
|
inspectBtn.onClick.AddListener(OnInspectClicked);
|
|
void OnInspectClicked()
|
|
{
|
|
if (!Value.IsNullOrDestroyed(false))
|
|
InspectorManager.Instance.Inspect(this.Value, this.Owner);
|
|
}
|
|
|
|
m_inspectButton.SetActive(false);
|
|
|
|
// value label
|
|
|
|
var labelObj = UIFactory.CreateLabel(m_valueContent, TextAnchor.MiddleLeft);
|
|
m_baseLabel = labelObj.GetComponent<Text>();
|
|
var labelLayout = labelObj.AddComponent<LayoutElement>();
|
|
labelLayout.flexibleWidth = 9000;
|
|
labelLayout.minHeight = 25;
|
|
|
|
m_subContentParent = subGroup;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|