diff --git a/src/UI/CacheObject/CacheObjectBase.cs b/src/UI/CacheObject/CacheObjectBase.cs index 76010f9..549b8a8 100644 --- a/src/UI/CacheObject/CacheObjectBase.cs +++ b/src/UI/CacheObject/CacheObjectBase.cs @@ -18,7 +18,7 @@ namespace UnityExplorer.UI.CacheObject { NotEvaluated, Exception, - NullValue, + //NullValue, Boolean, Number, String, @@ -38,6 +38,7 @@ namespace UnityExplorer.UI.CacheObject public object Value { get; protected set; } public Type FallbackType { get; protected set; } + public bool LastValueWasNull { get; private set; } public InteractiveValue IValue { get; private set; } public Type CurrentIValueType { get; private set; } @@ -55,7 +56,7 @@ namespace UnityExplorer.UI.CacheObject public virtual void SetFallbackType(Type fallbackType) { this.FallbackType = fallbackType; - GetValueLabel(); + this.ValueLabelText = GetValueLabel(); } // internals @@ -104,6 +105,7 @@ namespace UnityExplorer.UI.CacheObject public abstract void TrySetUserValue(object value); + // The only method which sets the CacheObjectBase.Value public virtual void SetValueFromSource(object value) { this.Value = value; @@ -111,88 +113,122 @@ namespace UnityExplorer.UI.CacheObject if (!Value.IsNullOrDestroyed()) Value = Value.TryCast(); - var prevState = State; ProcessOnEvaluate(); - if (State != prevState) - { - if (this.IValue != null) - { - // State has changed, need to return IValue - ReleaseIValue(); - SubContentShowWanted = false; - } - } - if (this.IValue != null) this.IValue.SetValue(Value); } - /// - /// Process the CacheMember state when the value has been evaluated (or re-evaluated) - /// protected virtual void ProcessOnEvaluate() { + var prevState = State; + if (HadException) + { + LastValueWasNull = true; State = ValueState.Exception; + } else if (Value.IsNullOrDestroyed()) - State = ValueState.NullValue; + { + LastValueWasNull = true; + State = GetStateForType(FallbackType); + } else { - var type = Value.GetActualType(); + LastValueWasNull = false; + State = GetStateForType(Value.GetActualType()); + } - if (type == typeof(bool)) - State = ValueState.Boolean; - else if (type.IsPrimitive || type == typeof(decimal)) - State = ValueState.Number; - else if (type == typeof(string)) - State = ValueState.String; - else if (type.IsEnum) - State = ValueState.Enum; - - // todo Color and ValueStruct - - else if (typeof(IDictionary).IsAssignableFrom(type)) - State = ValueState.Dictionary; - else if (typeof(IEnumerable).IsAssignableFrom(type)) - State = ValueState.Collection; - else - State = ValueState.Unsupported; + if (IValue != null) + { + // If we changed states (always needs IValue change) + // or if the value is null, and the fallback type isnt string (we always want to edit strings). + if (State != prevState || (State != ValueState.String && Value.IsNullOrDestroyed())) + { + // need to return IValue + ReleaseIValue(); + SubContentShowWanted = false; + } } // Set label text - GetValueLabel(); + this.ValueLabelText = GetValueLabel(); } - protected void GetValueLabel() + public ValueState GetStateForType(Type type) { - string label; + if (type == typeof(bool)) + return ValueState.Boolean; + else if (type.IsPrimitive || type == typeof(decimal)) + return ValueState.Number; + else if (type == typeof(string)) + return ValueState.String; + else if (type.IsEnum) + return ValueState.Enum; + + // todo Color and ValueStruct + + else if (typeof(IDictionary).IsAssignableFrom(type)) + return ValueState.Dictionary; + else if (typeof(IEnumerable).IsAssignableFrom(type)) + return ValueState.Collection; + else + return ValueState.Unsupported; + } + + protected string GetValueLabel() + { + string label = ""; + switch (State) { - case ValueState.NotEvaluated: - label = $"{NOT_YET_EVAL} ({SignatureHighlighter.Parse(FallbackType, true)})"; break; - case ValueState.Exception: - label = $"{LastException.ReflectionExToString()}"; break; + // bool and number dont want the label for the value at all case ValueState.Boolean: case ValueState.Number: - label = null; break; + return null; + + case ValueState.NotEvaluated: + return $"{NOT_YET_EVAL} ({SignatureHighlighter.Parse(FallbackType, true)})"; + + case ValueState.Exception: + return $"{LastException.ReflectionExToString()}"; + case ValueState.String: - string s = Value as string; - if (s.Length > 200) - s = $"{s.Substring(0, 200)}..."; - label = $"\"{s}\""; break; - case ValueState.NullValue: - label = $"{ToStringUtility.ToStringWithType(Value, FallbackType, true)}"; break; - case ValueState.Enum: + if (!LastValueWasNull) + { + string s = Value as string; + if (s.Length > 200) + s = $"{s.Substring(0, 200)}..."; + return $"\"{s}\""; + } + break; + case ValueState.Collection: + if (!LastValueWasNull) + { + if (Value is IList iList) + label = $"[{iList.Count}] "; + else if (Value is ICollection iCol) + label = $"[{iCol.Count}] "; + else + label = "[?] "; + } + break; + case ValueState.Dictionary: - case ValueState.ValueStruct: - case ValueState.Color: - case ValueState.Unsupported: - default: - label = ToStringUtility.ToStringWithType(Value, FallbackType, true); break; + if (!LastValueWasNull) + { + if (Value is IDictionary iDict) + label = $"[{iDict.Count}] "; + else + label = "[?] "; + } + break; } - this.ValueLabelText = label; + + // Cases which dont return will append to ToStringWithType + + return label += ToStringUtility.ToStringWithType(Value, FallbackType, true); } // Setting cell state from our model @@ -218,17 +254,20 @@ namespace UnityExplorer.UI.CacheObject switch (State) { case ValueState.Exception: - case ValueState.NullValue: + //case ValueState.NullValue: SetValueState(cell, ValueStateArgs.Default); break; case ValueState.Boolean: - SetValueState(cell, new ValueStateArgs(false, toggleActive:true, applyActive: CanWrite)); + SetValueState(cell, new ValueStateArgs(false, toggleActive: true, applyActive: CanWrite)); break; case ValueState.Number: SetValueState(cell, new ValueStateArgs(false, typeLabelActive: true, inputActive: true, applyActive: CanWrite)); break; case ValueState.String: - SetValueState(cell, new ValueStateArgs(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true)); + if (LastValueWasNull) + SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: true)); + else + SetValueState(cell, new ValueStateArgs(true, false, SignatureHighlighter.StringOrange, subContentButtonActive: true)); break; case ValueState.Enum: SetValueState(cell, new ValueStateArgs(true, subContentButtonActive: CanWrite)); @@ -237,10 +276,10 @@ namespace UnityExplorer.UI.CacheObject case ValueState.Dictionary: case ValueState.ValueStruct: case ValueState.Color: - SetValueState(cell, new ValueStateArgs(true, inspectActive: true, subContentButtonActive: true)); + SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull, subContentButtonActive: !LastValueWasNull)); break; case ValueState.Unsupported: - SetValueState(cell, new ValueStateArgs(true, inspectActive: true)); + SetValueState(cell, new ValueStateArgs(true, inspectActive: !LastValueWasNull)); break; } @@ -249,6 +288,7 @@ namespace UnityExplorer.UI.CacheObject protected virtual void SetValueState(CacheObjectCell cell, ValueStateArgs args) { + // main value label if (args.valueActive) { cell.ValueLabel.text = ValueLabelText; @@ -258,10 +298,12 @@ namespace UnityExplorer.UI.CacheObject else cell.ValueLabel.text = ""; + // Type label (for primitives) cell.TypeLabel.gameObject.SetActive(args.typeLabelActive); if (args.typeLabelActive) cell.TypeLabel.text = SignatureHighlighter.Parse(Value.GetActualType(), false); + // toggle for bools cell.Toggle.gameObject.SetActive(args.toggleActive); if (args.toggleActive) { @@ -270,6 +312,7 @@ namespace UnityExplorer.UI.CacheObject cell.ToggleText.text = Value.ToString(); } + // inputfield for numbers cell.InputField.gameObject.SetActive(args.inputActive); if (args.inputActive) { @@ -277,9 +320,13 @@ namespace UnityExplorer.UI.CacheObject cell.InputField.readOnly = !CanWrite; } + // apply for bool and numbers cell.ApplyButton.Button.gameObject.SetActive(args.applyActive); - cell.InspectButton.Button.gameObject.SetActive(args.inspectActive); - cell.SubContentButton.Button.gameObject.SetActive(args.subContentButtonActive); + + // Inspect and IValue (subcontent) buttons - only if last value not null. + cell.InspectButton.Button.gameObject.SetActive(args.inspectActive && !LastValueWasNull); + // allow IValue for null strings though. + cell.SubContentButton.Button.gameObject.SetActive(args.subContentButtonActive && (!LastValueWasNull || State == ValueState.String)); } // CacheObjectCell Apply diff --git a/src/UI/Utility/ToStringUtility.cs b/src/UI/Utility/ToStringUtility.cs index e5bcdb6..99b8e44 100644 --- a/src/UI/Utility/ToStringUtility.cs +++ b/src/UI/Utility/ToStringUtility.cs @@ -16,8 +16,8 @@ namespace UnityExplorer.UI.Utility internal static Dictionary toStringFormattedMethods = new Dictionary(); // string allocs - private static readonly StringBuilder _stringBuilder = new StringBuilder(16384); private const string nullString = "null"; + private const string nullUnknown = nullString + " (?)"; private const string destroyedString = "Destroyed"; private const string untitledString = "untitled"; @@ -25,28 +25,28 @@ namespace UnityExplorer.UI.Utility public static string ToStringWithType(object value, Type fallbackType, bool includeNamespace = true) { - if (value == null && fallbackType == null) - return nullString; + if (value.IsNullOrDestroyed() && fallbackType == null) + return nullUnknown; Type type = value?.GetActualType() ?? fallbackType; string richType = SignatureHighlighter.Parse(type, includeNamespace); - _stringBuilder.Clear(); + var sb = new StringBuilder(); if (value.IsNullOrDestroyed()) { if (value == null) { - _stringBuilder.Append(nullString); - AppendRichType(_stringBuilder, richType); - return _stringBuilder.ToString(); + sb.Append(nullString); + AppendRichType(sb, richType); + return sb.ToString(); } else // destroyed unity object { - _stringBuilder.Append(destroyedString); - AppendRichType(_stringBuilder, richType); - return _stringBuilder.ToString(); + sb.Append(destroyedString); + AppendRichType(sb, richType); + return sb.ToString(); } } @@ -58,57 +58,39 @@ namespace UnityExplorer.UI.Utility else if (name.Length > 50) name = $"{name.Substring(0, 50)}..."; - _stringBuilder.Append($"\"{name}\""); - AppendRichType(_stringBuilder, richType); + sb.Append($"\"{name}\""); + AppendRichType(sb, richType); } else if (type.FullName.StartsWith(eventSystemNamespace)) { // UnityEngine.EventSystem classes can have some obnoxious ToString results with rich text. - _stringBuilder.Append(richType); + sb.Append(richType); } else { var toString = ToString(value); - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - if (value is IList iList) - _stringBuilder.Append($"[{iList.Count}] "); - else - if (value is ICollection iCol) - _stringBuilder.Append($"[{iCol.Count}] "); - else - _stringBuilder.Append("[?] "); - } - else if (typeof(IDictionary).IsAssignableFrom(type)) - { - if (value is IDictionary iDict) - _stringBuilder.Append($"[{iDict.Count}] "); - else - _stringBuilder.Append("[?] "); - } - if (type.IsGenericType || toString == type.FullName || toString == $"{type.FullName} {type.FullName}" || toString == $"Il2Cpp{type.FullName}" || type.FullName == $"Il2Cpp{toString}") { - _stringBuilder.Append(richType); + sb.Append(richType); } else // the ToString contains some actual implementation, use that value. { // prune long strings unless they're unity structs // (Matrix4x4 and Rect can have some longs ones that we want to display fully) if (toString.Length > 100 && !(type.IsValueType && type.FullName.StartsWith("UnityEngine"))) - _stringBuilder.Append(toString.Substring(0, 100)); + sb.Append(toString.Substring(0, 100)); else - _stringBuilder.Append(toString); + sb.Append(toString); - AppendRichType(_stringBuilder, richType); + AppendRichType(sb, richType); } } - return _stringBuilder.ToString(); + return sb.ToString(); } private static void AppendRichType(StringBuilder sb, string richType)