// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using FlaxEditor.Scripting; using FlaxEngine; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors { /// /// Editable object values. /// [HideInEditor] public class ValueContainer : List { /// /// The has default value flag. Set if is valid and assigned. /// protected bool _hasDefaultValue; /// /// The default value used to show difference in the UI compared to the default object values. Used to revert modified properties. /// protected object _defaultValue; /// /// The has reference value flag. Set if is valid and assigned. /// protected bool _hasReferenceValue; /// /// The reference value used to show difference in the UI compared to the other object. Used by the prefabs system. /// protected object _referenceValue; /// /// The values source information from reflection. Used to update values. /// public readonly ScriptMemberInfo Info; /// /// Gets the values type. /// public ScriptType Type { get; private set; } /// /// Gets a value indicating whether single object is selected. /// public bool IsSingleObject => Count == 1; /// /// Gets a value indicating whether selected objects are different values. /// public bool HasDifferentValues { get { for (int i = 1; i < Count; i++) { if (!Equals(this[0], this[i])) return true; } return false; } } /// /// Gets a value indicating whether selected objects are different types. /// public bool HasDifferentTypes { get { if (Count < 2) return false; var theFirstType = TypeUtils.GetObjectType(this[0]); for (int i = 1; i < Count; i++) { if (theFirstType != TypeUtils.GetObjectType(this[i])) return true; } return false; } } /// /// Gets a value indicating whether any value in the collection is null. Returns false if collection is empty. /// public bool HasNull { get { for (int i = 0; i < Count; i++) { if (this[i] == null) return true; } return false; } } /// /// Gets a value indicating whether all values in the collection are null. Returns true if collection is empty. /// public bool IsNull { get { for (int i = 0; i < Count; i++) { if (this[i] != null) return false; } return true; } } /// /// Gets a value indicating whether this any value in the collection is of value type (eg. a structure, not a class type). Returns false if collection is empty. /// public bool HasValueType { get { for (int i = 0; i < Count; i++) { if (this[i] != null && TypeUtils.GetObjectType(this[i]).IsValueType) return true; } return false; } } /// /// Gets a value indicating whether this values container type is array. /// public bool IsArray => Type != ScriptType.Null && Type.IsArray; /// /// True if member or type has that marks it as obsolete. /// public bool IsObsolete { get; } /// /// Gets the values types array (without duplicates). /// public ScriptType[] ValuesTypes { get { if (Count == 1) return new[] { TypeUtils.GetObjectType(this[0]) }; return ConvertAll(TypeUtils.GetObjectType).Distinct().ToArray(); } } /// /// Initializes a new instance of the class. /// /// The member info. public ValueContainer(ScriptMemberInfo info) { Info = info; Type = Info.ValueType; IsObsolete = Info.HasAttribute(typeof(ObsoleteAttribute), true); } /// /// Gets a value indicating whether this instance has reference value assigned (see ). /// public bool HasReferenceValue => _hasReferenceValue; /// /// Gets the reference value used to show difference in the UI compared to the other object. Used by the prefabs system. /// public object ReferenceValue => _referenceValue; /// /// Gets a value indicating whether this instance has reference value and the any of the values in the contains is modified (compared to the reference). /// /// /// For prefabs system it means that object property has been modified compared to the prefab value. /// public bool IsReferenceValueModified { get { if (_hasReferenceValue) { if (_referenceValue is SceneObject referenceSceneObject && referenceSceneObject && referenceSceneObject.HasPrefabLink) { for (int i = 0; i < Count; i++) { if ((SceneObject)this[i] == referenceSceneObject) continue; if (this[i] == null || (this[i] is SceneObject valueSceneObject && valueSceneObject && valueSceneObject.PrefabObjectID != referenceSceneObject.PrefabObjectID)) return true; } } else { for (int i = 0; i < Count; i++) { if (!ValueEquals(this[i], _referenceValue)) return true; } } } return false; } } /// /// Gets a value indicating whether this instance has default value assigned (see ). /// public bool HasDefaultValue => _hasDefaultValue; /// /// Gets the default value used to show difference in the UI compared to the default value object. Used to revert modified properties. /// public object DefaultValue => _defaultValue; /// /// Gets a value indicating whether this instance has default value and the any of the values in the contains is modified (compared to the reference). /// public bool IsDefaultValueModified { get { if (_hasDefaultValue) { for (int i = 0; i < Count; i++) { if (!ValueEquals(this[i], _defaultValue)) return true; } } return false; } } private static bool ValueEquals(object objA, object objB) { // Special case for String (null string is kind of equal to empty string from the user perspective) if (objA == null && objB is string objBStr && objBStr.Length == 0) return true; return Newtonsoft.Json.Utilities.MiscellaneousUtils.ValueEquals(objA, objB); } /// /// Initializes a new instance of the class. /// /// The member info. /// The parent values. public ValueContainer(ScriptMemberInfo info, ValueContainer instanceValues) : this(info) { Capacity = instanceValues.Count; for (int i = 0; i < instanceValues.Count; i++) { Add(Info.GetValue(instanceValues[i])); } if (instanceValues._hasDefaultValue) { _defaultValue = Info.GetValue(instanceValues._defaultValue); _hasDefaultValue = true; } else { var defaultValueAttribute = Info.GetAttribute(); if (defaultValueAttribute != null) { _defaultValue = defaultValueAttribute.Value; _hasDefaultValue = true; if (_defaultValue != null && _defaultValue.GetType() != Type.Type) { // Workaround for DefaultValueAttribute that doesn't support certain value types storage if (Type.Type == typeof(sbyte)) _defaultValue = Convert.ToSByte(_defaultValue); else if (Type.Type == typeof(short)) _defaultValue = Convert.ToInt16(_defaultValue); else if (Type.Type == typeof(ushort)) _defaultValue = Convert.ToUInt16(_defaultValue); else if (Type.Type == typeof(uint)) _defaultValue = Convert.ToUInt32(_defaultValue); else if (Type.Type == typeof(ulong)) _defaultValue = Convert.ToUInt64(_defaultValue); else if (Type.Type == typeof(long)) _defaultValue = Convert.ToInt64(_defaultValue); } } } if (instanceValues._hasReferenceValue) { // If the reference value is set for the parent values but it's null object then skip it if (instanceValues._referenceValue == null && !instanceValues.Type.IsValueType) return; try { _referenceValue = Info.GetValue(instanceValues._referenceValue); _hasReferenceValue = true; } catch { // Ignore error if reference value has different type or is invalid for this member } } } /// /// Initializes a new instance of the class. /// /// The target custom type of the container values. Used to override the data. /// The other values container to clone. public ValueContainer(ScriptType customType, ValueContainer other) { Capacity = other.Capacity; AddRange(other); Info = other.Info; Type = customType; } /// /// Initializes a new instance of the class. /// /// The member info. /// The type. protected ValueContainer(ScriptMemberInfo info, ScriptType type) { Info = info; Type = type; } /// /// Sets the type. Use with caution. /// /// The type. public void SetType(ScriptType type) { Type = type; } /// /// Gets the custom attributes defined for the values source member. /// /// The attributes objects array. public virtual object[] GetAttributes() { return Info.GetAttributes(true); } /// /// Refreshes the specified instance values. /// /// The parent values. public virtual void Refresh(ValueContainer instanceValues) { if (instanceValues == null) throw new ArgumentNullException(); if (instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) this[i] = Info.GetValue(instanceValues[i]); } /// /// Sets the specified instance values. Refreshes this values container. /// /// The parent values. /// The value. public virtual void Set(ValueContainer instanceValues, object value) { if (instanceValues == null) throw new ArgumentNullException(); if (instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { Info.SetValue(instanceValues[i], value); this[i] = Info.GetValue(instanceValues[i]); } } /// /// Sets the specified instance values. Refreshes this values container. /// /// The parent values. /// The other values to set this container to. public virtual void Set(ValueContainer instanceValues, ValueContainer values) { if (instanceValues == null || values == null) throw new ArgumentNullException(); if (instanceValues.Count != Count || values.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { Info.SetValue(instanceValues[i], values[i]); this[i] = Info.GetValue(instanceValues[i]); } } /// /// Sets the specified instance values with the container values. /// /// The parent values. public virtual void Set(ValueContainer instanceValues) { if (instanceValues == null) throw new ArgumentNullException(); if (instanceValues.Count != Count) throw new ArgumentException(); for (int i = 0; i < Count; i++) { Info.SetValue(instanceValues[i], this[i]); } } /// /// Sets the default value of the container. /// /// The value. public virtual void SetDefaultValue(object value) { _defaultValue = value; _hasDefaultValue = true; } /// /// Refreshes the default value of the container. /// /// The parent value. public virtual void RefreshDefaultValue(object instanceValue) { _defaultValue = Info.GetValue(instanceValue); _hasDefaultValue = true; } /// /// Clears the default value of the container. /// public virtual void ClearDefaultValue() { _defaultValue = null; _hasDefaultValue = false; } /// /// Sets the reference value of the container. /// /// The value. public virtual void SetReferenceValue(object value) { _referenceValue = value; _hasReferenceValue = true; } /// /// Refreshes the reference value of the container. /// /// The parent value. public virtual void RefreshReferenceValue(object instanceValue) { _referenceValue = Info.GetValue(instanceValue); _hasReferenceValue = true; } /// /// Clears the reference value of the container. /// public virtual void ClearReferenceValue() { _referenceValue = null; _hasReferenceValue = false; } } }