From a1da85433e5fd4bf9a40162cda4d5b7195740fe7 Mon Sep 17 00:00:00 2001 From: VladV Date: Sat, 15 Jan 2022 19:25:12 +0300 Subject: [PATCH] Add property validators --- Editor.Extras/Validators.meta | 3 + .../Validators/MissingReferenceValidator.cs | 25 +++++++ .../MissingReferenceValidator.cs.meta | 3 + Editor.Extras/Validators/RequiredValidator.cs | 38 ++++++++++ .../Validators/RequiredValidator.cs.meta | 3 + Editor/Attributes.cs | 24 ++++++ Editor/Elements/TriPropertyElement.cs | 5 ++ .../TriPropertyValidationResultElement.cs | 43 +++++++++++ ...TriPropertyValidationResultElement.cs.meta | 3 + Editor/TriEditor.cs | 20 ++++- Editor/TriProperty.cs | 39 +++++++++- Editor/TriPropertyDefinition.cs | 17 +++++ Editor/TriPropertyTree.cs | 16 ++++ Editor/TriValidator.cs | 65 +++++++++++++++++ Editor/TriValidator.cs.meta | 3 + Editor/Utilities/TriDrawersUtilities.cs | 73 +++++++++++++++++++ README.md | 34 +++++++++ Runtime/Attributes/RequiredAttribute.cs | 10 +++ Runtime/Attributes/RequiredAttribute.cs.meta | 3 + 19 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 Editor.Extras/Validators.meta create mode 100644 Editor.Extras/Validators/MissingReferenceValidator.cs create mode 100644 Editor.Extras/Validators/MissingReferenceValidator.cs.meta create mode 100644 Editor.Extras/Validators/RequiredValidator.cs create mode 100644 Editor.Extras/Validators/RequiredValidator.cs.meta create mode 100644 Editor/Elements/TriPropertyValidationResultElement.cs create mode 100644 Editor/Elements/TriPropertyValidationResultElement.cs.meta create mode 100644 Editor/TriValidator.cs create mode 100644 Editor/TriValidator.cs.meta create mode 100644 Runtime/Attributes/RequiredAttribute.cs create mode 100644 Runtime/Attributes/RequiredAttribute.cs.meta diff --git a/Editor.Extras/Validators.meta b/Editor.Extras/Validators.meta new file mode 100644 index 0000000..8b2e17d --- /dev/null +++ b/Editor.Extras/Validators.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e710031376614be5903f943dc4c4d07f +timeCreated: 1642261774 \ No newline at end of file diff --git a/Editor.Extras/Validators/MissingReferenceValidator.cs b/Editor.Extras/Validators/MissingReferenceValidator.cs new file mode 100644 index 0000000..287bc5b --- /dev/null +++ b/Editor.Extras/Validators/MissingReferenceValidator.cs @@ -0,0 +1,25 @@ +using TriInspector; +using TriInspector.Validators; +using UnityEditor; + +[assembly: RegisterTriValueValidator(typeof(MissingReferenceValidator<>))] + +namespace TriInspector.Validators +{ + public class MissingReferenceValidator : TriValueValidator + where T : UnityEngine.Object + { + public override TriValidationResult Validate(TriValue propertyValue) + { + if (propertyValue.Property.TryGetSerializedProperty(out var serializedProperty) && + serializedProperty.propertyType == SerializedPropertyType.ObjectReference && + serializedProperty.objectReferenceValue == null && + serializedProperty.objectReferenceInstanceIDValue != 0) + { + return TriValidationResult.Warning($"{propertyValue.Property.DisplayName} is missing"); + } + + return TriValidationResult.Valid; + } + } +} \ No newline at end of file diff --git a/Editor.Extras/Validators/MissingReferenceValidator.cs.meta b/Editor.Extras/Validators/MissingReferenceValidator.cs.meta new file mode 100644 index 0000000..c379b81 --- /dev/null +++ b/Editor.Extras/Validators/MissingReferenceValidator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e6349ecb04a34792bd193b53f6cc0ca3 +timeCreated: 1642263604 \ No newline at end of file diff --git a/Editor.Extras/Validators/RequiredValidator.cs b/Editor.Extras/Validators/RequiredValidator.cs new file mode 100644 index 0000000..8274958 --- /dev/null +++ b/Editor.Extras/Validators/RequiredValidator.cs @@ -0,0 +1,38 @@ +using TriInspector.Validators; +using TriInspector; + +[assembly: RegisterTriAttributeValidator(typeof(RequiredValidator), ApplyOnArrayElement = true)] + +namespace TriInspector.Validators +{ + public class RequiredValidator : TriAttributeValidator + { + public override TriValidationResult Validate(TriProperty property) + { + if (property.FieldType == typeof(string)) + { + var isNull = string.IsNullOrEmpty((string) property.Value); + if (isNull) + { + var message = Attribute.Message ?? $"{property.DisplayName} is required"; + return TriValidationResult.Error(message); + } + } + else if (typeof(UnityEngine.Object).IsAssignableFrom(property.FieldType)) + { + var isNull = null == (UnityEngine.Object) property.Value; + if (isNull) + { + var message = Attribute.Message ?? $"{property.DisplayName} is required"; + return TriValidationResult.Error(message); + } + } + else + { + return TriValidationResult.Error("RequiredAttribute only valid on Object and String"); + } + + return TriValidationResult.Valid; + } + } +} \ No newline at end of file diff --git a/Editor.Extras/Validators/RequiredValidator.cs.meta b/Editor.Extras/Validators/RequiredValidator.cs.meta new file mode 100644 index 0000000..fe267cc --- /dev/null +++ b/Editor.Extras/Validators/RequiredValidator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 58b713b3023343c3b571cd1b3fa7fdab +timeCreated: 1642261781 \ No newline at end of file diff --git a/Editor/Attributes.cs b/Editor/Attributes.cs index deaf46e..dce818c 100644 --- a/Editor/Attributes.cs +++ b/Editor/Attributes.cs @@ -62,4 +62,28 @@ namespace TriInspector public Type ProcessorType { get; } } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class RegisterTriValueValidatorAttribute : Attribute + { + public RegisterTriValueValidatorAttribute(Type validatorType) + { + ValidatorType = validatorType; + } + + public Type ValidatorType { get; } + public bool ApplyOnArrayElement { get; set; } = true; + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public class RegisterTriAttributeValidatorAttribute : Attribute + { + public RegisterTriAttributeValidatorAttribute(Type validatorType) + { + ValidatorType = validatorType; + } + + public Type ValidatorType { get; } + public bool ApplyOnArrayElement { get; set; } + } } \ No newline at end of file diff --git a/Editor/Elements/TriPropertyElement.cs b/Editor/Elements/TriPropertyElement.cs index b13e5d2..d6bdd57 100644 --- a/Editor/Elements/TriPropertyElement.cs +++ b/Editor/Elements/TriPropertyElement.cs @@ -26,6 +26,11 @@ namespace TriInspector.Elements element = drawers[index].CreateElementInternal(property, element); } + if (property.HasValidators) + { + AddChild(new TriPropertyValidationResultElement(property)); + } + AddChild(element); } diff --git a/Editor/Elements/TriPropertyValidationResultElement.cs b/Editor/Elements/TriPropertyValidationResultElement.cs new file mode 100644 index 0000000..1c02ec7 --- /dev/null +++ b/Editor/Elements/TriPropertyValidationResultElement.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace TriInspector.Elements +{ + public class TriPropertyValidationResultElement : TriElement + { + private readonly TriProperty _property; + private IReadOnlyList _validationResults; + + public TriPropertyValidationResultElement(TriProperty property) + { + _property = property; + } + + public override bool Update() + { + var dirty = base.Update(); + + dirty |= GenerateValidationResults(); + + return dirty; + } + + private bool GenerateValidationResults() + { + if (_property.ValidationResults == _validationResults) + { + return false; + } + + _validationResults = _property.ValidationResults; + + RemoveAllChildren(); + + foreach (var result in _validationResults) + { + AddChild(new TriInfoBoxElement(result.Message, result.MessageType)); + } + + return true; + } + } +} \ No newline at end of file diff --git a/Editor/Elements/TriPropertyValidationResultElement.cs.meta b/Editor/Elements/TriPropertyValidationResultElement.cs.meta new file mode 100644 index 0000000..1e215fe --- /dev/null +++ b/Editor/Elements/TriPropertyValidationResultElement.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2e1128573c5f481189ccbc210cbf6b18 +timeCreated: 1642262467 \ No newline at end of file diff --git a/Editor/TriEditor.cs b/Editor/TriEditor.cs index 1a68aac..905060b 100644 --- a/Editor/TriEditor.cs +++ b/Editor/TriEditor.cs @@ -52,6 +52,21 @@ namespace TriInspector { Profiler.EndSample(); } + + Profiler.BeginSample("TriInspector.RunValidation()"); + try + { + if (_inspector.ValidationRequired) + { + _inspector.ValidationRequired = false; + + _inspector.RunValidation(); + } + } + finally + { + Profiler.EndSample(); + } EditorStack.Push(this); Profiler.BeginSample("TriInspector.DoLayout()"); @@ -65,7 +80,10 @@ namespace TriInspector EditorStack.Pop(); } - serializedObject.ApplyModifiedProperties(); + if (serializedObject.ApplyModifiedProperties()) + { + _inspector.RequestValidation(); + } if (_inspector.RepaintRequired) { diff --git a/Editor/TriProperty.cs b/Editor/TriProperty.cs index ca95bcb..dd4046d 100644 --- a/Editor/TriProperty.cs +++ b/Editor/TriProperty.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using TriInspector.Utilities; using UnityEditor; @@ -10,11 +11,15 @@ namespace TriInspector { public sealed class TriProperty : ITriPropertyParent { + private static readonly IReadOnlyList EmptyValidationResults = + new List(); + private readonly TriPropertyDefinition _definition; private readonly int _propertyIndex; private readonly ITriPropertyParent _parent; [CanBeNull] private readonly SerializedProperty _serializedProperty; private List _childrenProperties; + private List _validationResults; private GUIContent _displayNameBackingField; @@ -36,6 +41,9 @@ namespace TriInspector Update(); } + [PublicAPI] + public string DisplayName => DisplayNameContent.text; + [PublicAPI] public GUIContent DisplayNameContent { @@ -137,6 +145,11 @@ namespace TriInspector public ITriPropertyParent Parent => _parent; + public bool HasValidators => _definition.Validators.Count != 0; + + public IReadOnlyList ValidationResults => + _validationResults ?? EmptyValidationResults; + [PublicAPI] public bool IsExpanded { @@ -200,7 +213,10 @@ namespace TriInspector public void SetValue(object value) { // save any pending changes - PropertyTree.SerializedObject.ApplyModifiedProperties(); + if (PropertyTree.SerializedObject.ApplyModifiedProperties()) + { + PropertyTree.RequestValidation(); + } // record object state for undp Undo.RegisterCompleteObjectUndo(PropertyTree.TargetObjects, "Inspector"); @@ -215,6 +231,8 @@ namespace TriInspector // actualize PropertyTree.SerializedObject.Update(); Update(); + + PropertyTree.RequestValidation(); } internal void Update() @@ -301,6 +319,25 @@ namespace TriInspector } } + internal void RunValidation() + { + if (HasValidators) + { + _validationResults = _definition.Validators + .Select(it => it.Validate(this)) + .Where(it => it.MessageType != MessageType.None) + .ToList(); + } + + if (_childrenProperties != null) + { + foreach (var childrenProperty in _childrenProperties) + { + childrenProperty.RunValidation(); + } + } + } + [PublicAPI] public bool TryGetSerializedProperty(out SerializedProperty serializedProperty) { diff --git a/Editor/TriPropertyDefinition.cs b/Editor/TriPropertyDefinition.cs index 1dec5e1..eb61355 100644 --- a/Editor/TriPropertyDefinition.cs +++ b/Editor/TriPropertyDefinition.cs @@ -17,6 +17,7 @@ namespace TriInspector private TriPropertyDefinition _arrayElementDefinitionBackingField; private IReadOnlyList _drawersBackingField; + private IReadOnlyList _validatorsBackingField; private IReadOnlyList _hideProcessorsBackingField; private IReadOnlyList _disableProcessorsBackingField; @@ -117,6 +118,22 @@ namespace TriInspector } } + public IReadOnlyList Validators + { + get + { + if (_validatorsBackingField == null) + { + _validatorsBackingField = Enumerable.Empty() + .Concat(TriDrawersUtilities.CreateValueValidatorsFor(FieldType)) + .Concat(TriDrawersUtilities.CreateAttributeValidatorsFor(Attributes)) + .ToList(); + } + + return _validatorsBackingField; + } + } + public object GetValue(TriProperty property, int targetIndex) { var parentValue = property.Parent.GetValue(targetIndex); diff --git a/Editor/TriPropertyTree.cs b/Editor/TriPropertyTree.cs index 2e856cd..6d3fbbf 100644 --- a/Editor/TriPropertyTree.cs +++ b/Editor/TriPropertyTree.cs @@ -30,6 +30,8 @@ namespace TriInspector }) .ToList(); + ValidationRequired = true; + _mode = mode; _inspectorElement = new TriInspectorElement(this); _inspectorElement.AttachInternal(); @@ -52,6 +54,7 @@ namespace TriInspector public bool IsInlineEditor => (_mode & TriEditorMode.InlineEditor) != 0; internal bool RepaintRequired { get; set; } + internal bool ValidationRequired { get; set; } object ITriPropertyParent.GetValue(int targetIndex) => TargetObjects[targetIndex]; @@ -81,6 +84,14 @@ namespace TriInspector _inspectorElement.Update(); } + internal void RunValidation() + { + foreach (var property in Properties) + { + property.RunValidation(); + } + } + internal void DoLayout() { var width = EditorGUIUtility.currentViewWidth; @@ -93,6 +104,11 @@ namespace TriInspector { RepaintRequired = true; } + + public void RequestValidation() + { + ValidationRequired = true; + } } [Flags] diff --git a/Editor/TriValidator.cs b/Editor/TriValidator.cs new file mode 100644 index 0000000..5965304 --- /dev/null +++ b/Editor/TriValidator.cs @@ -0,0 +1,65 @@ +using System; +using JetBrains.Annotations; +using UnityEditor; + +namespace TriInspector +{ + public abstract class TriValidator + { + internal bool ApplyOnArrayElement { get; set; } + + [PublicAPI] + public abstract TriValidationResult Validate(TriProperty property); + } + + public abstract class TriAttributeValidator : TriValidator + { + internal Attribute RawAttribute { get; set; } + } + + public abstract class TriAttributeValidator : TriAttributeValidator + where TAttribute : Attribute + { + [PublicAPI] + public TAttribute Attribute => (TAttribute) RawAttribute; + } + + public abstract class TriValueValidator : TriValidator + { + } + + public abstract class TriValueValidator : TriValueValidator + { + public sealed override TriValidationResult Validate(TriProperty property) + { + return Validate(new TriValue(property)); + } + + [PublicAPI] + public abstract TriValidationResult Validate(TriValue propertyValue); + } + + public readonly struct TriValidationResult + { + public static TriValidationResult Valid => new TriValidationResult(null, MessageType.None); + + private TriValidationResult(string message, MessageType messageType) + { + Message = message; + MessageType = messageType; + } + + public string Message { get; } + public MessageType MessageType { get; } + + public static TriValidationResult Error(string error) + { + return new TriValidationResult(error, MessageType.Error); + } + + public static TriValidationResult Warning(string error) + { + return new TriValidationResult(error, MessageType.Warning); + } + } +} \ No newline at end of file diff --git a/Editor/TriValidator.cs.meta b/Editor/TriValidator.cs.meta new file mode 100644 index 0000000..90efe43 --- /dev/null +++ b/Editor/TriValidator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b90ba6bdef614e9aa246f371506ea113 +timeCreated: 1642260754 \ No newline at end of file diff --git a/Editor/Utilities/TriDrawersUtilities.cs b/Editor/Utilities/TriDrawersUtilities.cs index f654bd4..cfcc13b 100644 --- a/Editor/Utilities/TriDrawersUtilities.cs +++ b/Editor/Utilities/TriDrawersUtilities.cs @@ -12,6 +12,8 @@ namespace TriInspector.Utilities private static IDictionary _allGroupDrawersCacheBackingField; private static IReadOnlyList _allAttributeDrawerTypesBackingField; private static IReadOnlyList _allValueDrawerTypesBackingField; + private static IReadOnlyList _allAttributeValidatorTypesBackingField; + private static IReadOnlyList _allValueValidatorTypesBackingField; private static IReadOnlyList _allHideProcessorTypesBackingField; private static IReadOnlyList _allDisableProcessorTypesBackingField; @@ -72,6 +74,42 @@ namespace TriInspector.Utilities } } + public static IReadOnlyList AllValueValidatorTypes + { + get + { + if (_allValueValidatorTypesBackingField == null) + { + _allValueValidatorTypesBackingField = ( + from asm in TriReflectionUtilities.Assemblies + from attr in asm.GetCustomAttributes() + where IsValueValidatorType(attr.ValidatorType, out _) + select attr + ).ToList(); + } + + return _allValueValidatorTypesBackingField; + } + } + + public static IReadOnlyList AllAttributeValidatorTypes + { + get + { + if (_allAttributeValidatorTypesBackingField == null) + { + _allAttributeValidatorTypesBackingField = ( + from asm in TriReflectionUtilities.Assemblies + from attr in asm.GetCustomAttributes() + where IsAttributeValidatorType(attr.ValidatorType, out _) + select attr + ).ToList(); + } + + return _allAttributeValidatorTypesBackingField; + } + } + public static IReadOnlyList AllHideProcessors { get @@ -133,6 +171,16 @@ namespace TriInspector.Utilities return TryGetBaseGenericTargetType(type, typeof(TriAttributeDrawer<>), out attributeType); } + private static bool IsValueValidatorType(Type type, out Type valueType) + { + return TryGetBaseGenericTargetType(type, typeof(TriValueValidator<>), out valueType); + } + + private static bool IsAttributeValidatorType(Type type, out Type attributeType) + { + return TryGetBaseGenericTargetType(type, typeof(TriAttributeValidator<>), out attributeType); + } + private static bool IsHideProcessorType(Type type, out Type attributeType) { return TryGetBaseGenericTargetType(type, typeof(TriPropertyHideProcessor<>), out attributeType); @@ -171,6 +219,31 @@ namespace TriInspector.Utilities }); } + public static IEnumerable CreateValueValidatorsFor(Type valueType) + { + return + from validator in AllValueValidatorTypes + where IsValueValidatorType(validator.ValidatorType, out var vt) && + IsValidTargetType(vt, valueType) + select CreateInstance(validator.ValidatorType, valueType, + it => { it.ApplyOnArrayElement = validator.ApplyOnArrayElement; }); + } + + public static IEnumerable CreateAttributeValidatorsFor( + IReadOnlyList attributes) + { + return + from attribute in attributes + from validator in AllAttributeValidatorTypes + where IsAttributeValidatorType(validator.ValidatorType, out var vt) && + IsValidTargetType(vt, attribute.GetType()) + select CreateInstance(validator.ValidatorType, attribute.GetType(), it => + { + it.ApplyOnArrayElement = validator.ApplyOnArrayElement; + it.RawAttribute = attribute; + }); + } + public static IEnumerable CreateHideProcessorsFor(IReadOnlyList attributes) { return diff --git a/README.md b/README.md index 57afa3d..82589bd 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ public class BasicSample : TriMonoBehaviour [PropertySpace(SpaceBefore = 10, SpaceAfter = 20)] [PropertyTooltip("My Tooltip")] public float unityField; + + [Required] + public Material mat; [InlineEditor] public SampleScriptableObject objectReference; @@ -133,6 +136,37 @@ public class TriBoxGroupDrawer : TriGroupDrawer } ``` +Custom Value Validator +```csharp +using TriInspector; + +[assembly: RegisterTriValueValidator(typeof(MissingReferenceValidator<>))] + +public class MissingReferenceValidator : TriValueValidator + where T : UnityEngine.Object +{ + public override TriValidationResult Validate(TriValue propertyValue) + { + // ... + } +} +``` + +Custom Attribute Validator +```csharp +using TriInspector; + +[assembly: RegisterTriAttributeValidator(typeof(RequiredValidator), ApplyOnArrayElement = true)] + +public class RequiredValidator : TriAttributeValidator +{ + public override TriValidationResult Validate(TriProperty property) + { + // ... + } +} +``` + Property Hide Processor ```csharp using TriInspector; diff --git a/Runtime/Attributes/RequiredAttribute.cs b/Runtime/Attributes/RequiredAttribute.cs new file mode 100644 index 0000000..0808e6a --- /dev/null +++ b/Runtime/Attributes/RequiredAttribute.cs @@ -0,0 +1,10 @@ +using System; + +namespace TriInspector +{ + [AttributeUsage((AttributeTargets.Field | AttributeTargets.Property))] + public class RequiredAttribute : Attribute + { + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/Runtime/Attributes/RequiredAttribute.cs.meta b/Runtime/Attributes/RequiredAttribute.cs.meta new file mode 100644 index 0000000..13be2b6 --- /dev/null +++ b/Runtime/Attributes/RequiredAttribute.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d886b45ac8b1474db5cdfd07c859e616 +timeCreated: 1642261718 \ No newline at end of file